Ultra-practical high-concurrency programming ExecutorCompletionService case analysis and source code interpretation

Explanation

Suppose there are a large number of tasks that need to be calculated. In order to improve the execution efficiency of the entire batch of tasks, you may use the thread pool to continuously submit asynchronous calculation tasks to the thread pool. At the same time, you need to keep the Future associated with each task, and finally traverse These Futures obtain the results of the entire batch of computing tasks by calling the get method of the Future interface implementation class.

Although the thread pool is used to improve the overall execution efficiency, traversing these Futures and calling the get method of the Future interface implementation class is blocked, that is, when the computing tasks associated with the current Future are actually executed, the get method returns the result , if the current calculation task is not completed, but other calculation tasks associated with the Future have been completed, a lot of waiting time will be wasted, so it is best to obtain the result first when traversing, so that Saved a lot of waiting time.

The ExecutorCompletionService can achieve such an effect. It has a first-in-first-out blocking queue inside to save the Future that has been executed. By calling its take method or poll method, you can get a Future that has been executed, and then pass Call the get method of the Future interface implementation class to obtain the final result.

Example demonstration

@Test
public void test() throws InterruptedException, ExecutionException {
    Executor executor = Executors.newFixedThreadPool(3);
    CompletionService<String> service = new ExecutorCompletionService<>(executor);
    for (int i = 0 ; i < 5 ;i++) {
        int seqNo = i;
        service.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "HelloWorld-" + seqNo + "-" + Thread.currentThread().getName();
            }
        });
    }
    for (int j = 0 ; j < 5; j++) {
        System.out.println(service.take().get());
    }
}

Results of the:

HelloWorld-2-pool-1-thread-3
HelloWorld-1-pool-1-thread-2
HelloWorld-3-pool-1-thread-2
HelloWorld-4-pool-1-thread-3
HelloWorld-0-pool-1-thread-1

method analysis

ExecutorCompletionService implements the CompletionService interface, and the following methods are defined in the CompletionService interface:

  • Future submit(Callable task): Submit a Callable task and return the Future associated with the task execution result;
  • Future submit(Runnable task, V result): Submit a Runnable task and return the Future associated with the task execution result;
  • Future take(): Obtain and remove the first completed task from the internal blocking queue, block until a task is completed;
  • Future poll(): Obtain and remove the first completed task from the internal blocking queue, and return null if not obtained, without blocking;
  • Future poll(long timeout, TimeUnit unit): Obtain and remove the first completed task from the internal blocking queue. The blocking time is timeout. If it cannot be obtained, it will return null;

Source code analysis

Analyze the internal implementation principle of ExecutorCompletionService according to the above example demonstration code.

ExecutorCompletionService has three private properties, namely executor, aes and completionQueue, where completionQueue is the queue for storing completed tasks. The specific code is as shown in the figure below.

Enter its construction method and assign values ​​to its three attributes inside the method. You can see that a first-in-first-out blocking queue of LinkedBlockingQueue type is initialized here. The specific code is as shown in the figure below.

Next, enter the submit method of ExecutorCompletionService. Here we analyze the submit method whose parameter type is Callable. The specific code is as shown in the figure below.

The tracking code enters the newTaskFor method, and the specific code is as shown in the figure below.

In the ExecutorCompletionService construction method, aes has been assigned a value, so enter the newTaskFor method of AbstractExecutorService, the specific code is as shown in the figure below.

The tracking code enters the FutureTask construction method, and the specific code is as shown in the figure below.

The RunnableFuture instance object built here is completed. Go back to the above submit method and continue to analyze executor.execute(new QueueingFuture(f)), first of all, new QueueingFuture(f). QueueingFuture is an internal class in ExecutorCompletionService. The specific code is as follows picture.

As can be seen from the code in the figure, the RunnableFuture instance object is assigned to the task property of QueueingFuture. Note that there is a done method in the red box in the above figure. Its internal is to add a task to the completed blocking queue. This is first recorded I will use it later. Next, analyze executor.execute(new QueueingFuture(f)), because ThreadPoolExecutor is used in our sample demonstration code, so the executor.execute() method is executed in ThreadPoolExecutor, the specific key code is as shown in the figure below.

Here we do not analyze extreme cases. When the number of worker threads is less than the number of core threads, the addWorker method is executed. The body of this method has a lot of content. Here we only focus on the key codes. The specific codes are shown in the figure below.

The code in the first red box will build a Worker instance, the specific code is as shown in the figure below.

According to the code in the red box in the above figure, if you continue to trace the code, you will find that the t.start() method will be executed in the run method in the above figure, and the runWorker method is executed inside the run method. The specific code is as shown in the figure below.

If you continue to trace the code in the above figure, you can find that executing task.run() will enter the run method of the previously constructed RunnableFuture instance object. The specific code is as shown in the figure below.

The code in the first red box is the code for the actual task execution, which is where the task submitted by submit is actually executed. The code in the second red box is the processing when an exception occurs, and the code in the third red box is the processing completed by normal execution. The following is their specific implementation code.

From the codes in the above two figures, it is found that the finishCompletion() method is executed. Let’s reveal the function of this method. The specific code is as shown in the figure below.

As can be seen from the code in the red box in the above figure, the done() method is executed here, and what is actually executed is the done method that adds a task to the completed blocking queue mentioned in our previous analysis. So far, when a task is completed or abnormal, it will be added to the completed blocking queue, and then taken out for processing.

Next, analyze the take method and poll method in ExecutorCompletionService. The specific code is shown in the figure below.

As can be seen from the figure above, all operations have completed the blocking queue, so let's take a look at the code in the completed blocking queue, as shown in the figure below.

The above figure clearly shows that the execution task is completed by waiting for the loop.

The above code does not block, and returns null directly when there is no completed execution task.

The above code blocks for a specified time, and returns null directly when there is no completed execution task.

Guess you like

Origin blog.csdn.net/dyuan134/article/details/130243449