Interviewer: Tell me about thread pools

Posted by Shit4Brains on Wed, 22 Dec 2021 07:56:48 +0100

Scene Review

Today I went back to my dormitory and singed: Cut me out soon ~, which frightened me to get to know the situation quickly. I thought I was having trouble with my girlfriend. As a result, the interview was abused by the interviewer again today, so I had to delete the interviewer.

Interviewer: I see that I have written concurrent programming proficiency in my resume. Thread pools must be used in my usual work. What scenarios do you usually use?

Lobule: Well, thread pools are used normally. I use them in crawl scenarios where multiple network requests can be processed in parallel through thread pools, which can improve the throughput of the system.

Interviewer: Well, thread pools are a common scenario for crawlers. How did you create them?

Leaf: I usually use the method provided by JDK's own factory method to create thread pools directly. I don't think it's a big name.

Interviewer: You are talking about the Executors class. Usually we use a single-threaded thread pool, a fixed-threaded thread pool, and so on. How does it create different thread pools with parameters at the bottom?

Lobule: emm... You are saying ThreadPoolExecutor. It seems to consist of seven parameters, such as number of core threads, maximum threads, length of idle time, unit of idle time, task queue, thread factory, rejection policy.

Interviewer: Well, it looks like you still know a little about thread pools. If I now have 10 core threads, 20 maximum threads, and an unlimited queue of tasks, and now I have 30 tasks at once, how many threads are in the current thread pool, will the tasks be rejected?

Lobule: This is simple. The maximum number of threads is 20 and there are 30 tasks coming at once. Of course, the number of threads is 20, because the task queue is unbounded, the task will not be rejected.

Interviewer: Well, if I change the task queue to a size of 20, how many requests can I receive at most now?

Lobule: emm... Should be core threads + maximum threads + task queue size = 50.

Interviewer: Today's interview is here first.

Through the miserable interview with Lobule, it is not difficult to find that the thread pool is a question that is frequently asked by the interviewer. A few simple parameters of the thread pool may not be the result we expected. And we may call the simple methods provided by the Executors class directly for the sake of convenience during normal encoding, but there may be some problems that need to be considered. All task queues in the thread pool methods provided in the Executors class are unbounded, which may cause memory overflow. And I want to use other task rejection strategies, how do we do that? With these questions in mind, here is a small strip that takes you from shallow to deep.

Overall structure

From the diagram above, you can see the overall inheritance relationship of ThreadPoolExecutor, where you can see the author of the design thread pool, designed two interfaces separated by their responsibilities, and added hook functions through a template approach.

Executor interface

public interface Executor {
    // How a task is executed depends on how the subclasses are implemented
    void execute(Runnable command);
}

ExecutorService interface

public interface ExecutorService extends Executor {
        // Stop the thread pool until all tasks in the pool have been executed
    void shutdown();
       // Stop the thread pool immediately and return unexecuted tasks
    List<Runnable> shutdownNow();
        // Stop state
    boolean isShutdown();
    // End or not
    boolean isTerminated();
    // Waiting for thread pool to stop
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;
        // Submit a task with a return result and return a proxy object for the result
    <T> Future<T> submit(Callable<T> task);
       // Submit task without return, task execution successfully returns result
    <T> Future<T> submit(Runnable task, T result);
    // Submit tasks that do not return
    Future<?> submit(Runnable task);
        // Completes all tasks in the tasks collection and returns the results
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
    // Add timeout based on previous method
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;
    // Return once any task in tasks has been executed
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;
        // Timeout added in previous step
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

ExecutorService describes that a thread pool should have functions such as closing the thread pool, submitting tasks, task execution policies, and so on.

Next, we'll look at how thread pools reduce code through template methods using the AbstractExecutorService Abstract class.

The structure diagram of the class shows that the abstract class implements the methods of submit, invokeAll, invokeAny.

submit

public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
          // NewTaskForThis method is not covered here, it can be understood as wrapping the task
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        // Call methods in Executor class
        execute(ftask);
        return ftask;
}

The source code shows that this method wraps the task and calls the execute method directly.

invokeAll

public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                         long timeout, TimeUnit unit)
        throws InterruptedException {
        if (tasks == null)
            throw new NullPointerException();
        // Calculate timeout
        long nanos = unit.toNanos(timeout);
        // Collection for task storage
        ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
        boolean done = false;
        try {
            for (Callable<T> t : tasks)
                futures.add(newTaskFor(t));
            // Calculate Task Execution Deadline
            final long deadline = System.nanoTime() + nanos;
            final int size = futures.size();
            // task in the execution collection of a loop
            for (int i = 0; i < size; i++) {
                execute((Runnable)futures.get(i));
                // Determine whether the deadline has been reached
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L)
                    return futures;
            }
            // The code runs here, indicating that all tasks have been executed within the specified time, but the processing results are unknown
            for (int i = 0; i < size; i++) {
                Future<T> f = futures.get(i);
                // Get the return value agent of the task to determine if it is completed
                if (!f.isDone()) {
                    // Return to the future collection if the deadline has been reached
                    if (nanos <= 0L)
                        return futures;
                    try {
                        // Block waiting for future to return
                        f.get(nanos, TimeUnit.NANOSECONDS);
                    } catch (CancellationException ignore) {
                    } catch (ExecutionException ignore) {
                    } catch (TimeoutException toe) {
                        // Return future s collection if timed out
                        return futures;
                    }
                    // Update Remaining Time
                    nanos = deadline - System.nanoTime();
                }
            }
            // The code runs here to indicate that all tasks have been performed and to set the completion flag
            done = true;
            return futures;
        } finally {
            /** Cancel all tasks in progress if don is false
                Only a time-out during task submission and a time-out to get an execution result will be false
            **/
            if (!done)
                for (int i = 0, size = futures.size(); i < size; i++)
                    futures.get(i).cancel(true);
        }
    }

invokeAny

private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
                              boolean timed, long nanos)
        throws InterruptedException, ExecutionException, TimeoutException {
        // Empty check
        if (tasks == null)
            throw new NullPointerException();
        int ntasks = tasks.size();
        if (ntasks == 0)
            throw new IllegalArgumentException();
        ArrayList<Future<T>> futures = new ArrayList<Future<T>>(ntasks);
        
        /**
            This class is read outside the JUC package and provides an overview of what this class provides.
            The thread pool instance is passed to this class so that it can call the execute method of the thread pool without having to maintain the pool itself.
            This class implements submit submission tasks and returns furure, and maintains a blocking queue internally where the future object is placed when the task executes successfully
            take Method is get element of queue blocking
            poll Method is a non-blocking get element of the queue
        **/
        ExecutorCompletionService<T> ecs =
            new ExecutorCompletionService<T>(this);

        try {
            // Record exceptions
            ExecutionException ee = null;
            // Calculate timeout if on timeout
            final long deadline = timed ? System.nanoTime() + nanos : 0L;
            Iterator<? extends Callable<T>> it = tasks.iterator();

            // Will be the first task
            futures.add(ecs.submit(it.next()));
            // Number of tasks-1
            --ntasks;
            int active = 1;

            for (;;) {
                // Non-blocking Get Element
                Future<T> f = ecs.poll();
                if (f == null) {
                    // If there are still tasks to execute at this time, continue committing them and add activity + 1
                    if (ntasks > 0) {
                        --ntasks;
                        futures.add(ecs.submit(it.next()));
                        ++active;
                    }
                    // The active number is 0 to exit the cycle, and the active number is 0 to move from future.get() Gets the throw exception to execute here
                    else if (active == 0)
                        break;
                    else if (timed) {
                        // If timeout is on, wait in response queue for element to be fetched, timeout error
                        f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
                        if (f == null)
                            throw new TimeoutException();
                        nanos = deadline - System.nanoTime();
                    }
                    else
                        // Unopened timeout enters an infinite waiting block to get a response
                        f = ecs.take();
                }
                // If the response queue is not empty, a task is completed at this time
                if (f != null) {
                    // Active count-1
                    --active;
                    /**
                        Get response results through future, if returned directly
                        If the get result fails to record an exception, continue traversing to get the next successful result from the response queue
                    **/
                    try {
                        return f.get();
                    } catch (ExecutionException eex) {
                        ee = eex;
                    } catch (RuntimeException rex) {
                        ee = new ExecutionException(rex);
                    }
                }
            }
            
            // Executing here means that all tasks are completed and all future. Getts fail, exceptions will be thrown
            if (ee == null)
                ee = new ExecutionException();
            throw ee;

        } finally {
            // Finally, undo all tasks once
            for (int i = 0, size = futures.size(); i < size; i++)
                futures.get(i).cancel(true);
        }
    }

Final conclusion

Readers who can read this are confident that you have mastered a very important method of base classes in the thread pool. At least you know how to increase code reuse by designing base classes, and invokeAny logic execution is interesting. Reading source code is a tedious process, but your thinking evolves with the big guys step by step. That's why Internet companies have asked to read the source code. In the next post, I'll officially enter the ThreadPoolExecutor class to unravel its mystery and the interviewer's questions will be answered.

This article is published by blog OpenWrite Release!

Topics: Java