How is thread reuse in a Java thread pool implemented?

Posted by denniston on Tue, 16 Jun 2020 18:25:54 +0200

A few days ago, a group member of the technical group asked a question about the thread pool, as shown in the figure:


So let's talk about this. In a thread pool, threads read tasks from workQueue to execute. The smallest unit of execution is Worker, which implements the Runnable interface and overrides the run method.The method is to let each thread execute a loop in which the code determines if there is a task to execute and if so, executes it directly, so the number of threads does not increase.

The following is the overall flowchart of the thread pool creation thread:


The first step is to determine the state of the thread pool, that is, whether it is running or not, and reject it if it is not.The next step is to determine if the number of threads is less than the number of core threads. If it is less than the number of core threads, new worker threads will be created and tasks will be executed. As tasks increase, the number of threads will slowly increase to the number of core threads. If tasks are committed at this time, the blocked queue workQueue will be determinedIf it is full or not, it will put the task in the blocked queue and wait for the worker threads to get and execute. If the task submits so much that the blocked queue reaches its maximum, it will determine if the number of threads is less than the maximum number of threads.maximumPoolSize, if less than the maximum number of threads, the thread pool adds worker threads and executes tasks. If there are still a large number of task submissions, making the number of threads equal to the maximum number of threads, task submissions will be rejected at this time.

Now that we have a general understanding of this process, let's see how the source code is implemented.

Task submission for thread pools is defined by the AbstractExecutorService abstract class from the submit method, which does two main things:

  1. Convert both Runnable and Allable to FutureTask

  2. Execute FutureTask using execute method

The execute method is the method in ThreadPoolExecutor with the following source code:

public void execute(Runnable command) {
   // If the task is empty, throw NPE and cannot execute the empty task
   if (command == null) {
       throw new NullPointerException();
   }
   int c = ctl.get();
   // If the number of worker threads is less than the number of core threads, a new thread is created and the current task is added command As the first task of this thread
   if (workerCountOf(c) < corePoolSize) {
       if (addWorker(command, true)) {
           return;
       }
       c = ctl.get();
   }
   /**
       * So far, there are two situations:
       * 1.Current number of worker threads is greater than or equal to the number of core threads
       * 2.New Thread Failed
       * This will attempt to add the task to the blocked queue workQueue
       */
   // If the thread pool is in RUNNING state, add tasks to the blocking queue workQueue
   if (isRunning(c) && workQueue.offer(command)) {
       // Check thread pool tag again
       int recheck = ctl.get();
       // If the thread pool is no longer in RUNNING state, remove the queued tasks and execute the rejection policy
       if (!isRunning(recheck) && remove(command)) {
           // Task added to blocked queue failed to execute rejection policy
           reject(command);
       }
       // If the thread pool is RUNNING and the number of threads is 0, start a new thread
       else if (workerCountOf(recheck) == 0) {
           addWorker(null, false);
       }
   }
   /**
       * So far, there are two situations:
       * 1.Thread pool is not running, thread pool no longer accepts new threads
       * 2.The thread is running, but the blocking queue is full and cannot join the blocking queue
       * An attempt is made to create a new worker thread bounded by the maximum number of threads
       */
   else if (!addWorker(command, false)) {
       // Task failed to enter thread pool, executing rejection policy
       reject(command);
   }
}

You can see that the core method in the execute method is addWorker, and before you go to the addWorker method, look at the initialization method of Worker:

Worker(Runnable firstTask) {
   // The lock state of each task is initialized to-1,This prevents the worker thread from interrupting before running
   setState(-1);
   this.firstTask = firstTask;
   // hold Worker As thread Running Tasks
   this.thread = getThreadFactory().newThread(this);
}

The current Worker is included as the constructor of the thread when the Worker is initialized, and the following code can be found from the addWorker method:

final Thread t = w.thread;
// If you add Worker successfully, you can start the Worker
if (workerAdded) {
   t.start();
   workerStarted = true;
}

This code is successful in adding the worker, calling the start method to start the thread, Thread = w.thread; W is a reference to the worker, then t.start(); in fact, it executes the run method of the worker.

The runWorker method is called in Worker's run method, and the simplified runWorker source code is as follows:

final void runWorker(Worker w) {
   Runnable task = w.firstTask;
   while (task != null || (task = getTask()) != null) {
       try {
           task.run();
       } finally {
           task = null;
       }
   }
}

This while loop has a getTask method whose main function is to block tasks from the queue. If there are tasks in the queue, they can be taken out to execute. If there are no tasks in the queue, the thread will be blocked until the task (or timeout blocked), where the getTask method has the following sequence diagram:


The key to thread reuse is the 1.6 and 1.7 parts, which are source codes as follows:

Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();

Use the poll or take method of the queue to retrieve data from the queue. Depending on the characteristics of the queue, tasks in the queue can be returned and no tasks in the queue can be blocked.

Thread pool thread reuse implements thread reuse by taking Worker's firstTask or getTask method to fetch tasks from workQueue and directly calling Runnable's run method to execute tasks, which ensures that each thread always retrieves tasks repeatedly in a loop and then executes them.

summary

This article mainly explains how thread reuse is implemented in the Java thread pool from the source code point of view.Welcome to leave a message for discussion.


Topics: Java less