This article will analyze the implementation principles of The Java thread pool ThreadPoolExecutor. Knowing the implementation principles can help you optimize the performance of your application and avoid many errors. The amount of code in this article is less, mainly about the principle, you completely understand the principle of the code to write a lot of random.

1. Status of the thread pool

First, a thread pool is a stateful object. There are the following states:

· RUNNING: RUNNING. At this point, the thread pool can accept the task and will process the task in the queue;

· SHUTDOWN: SHUTDOWN. At this point, the thread pool does not accept new tasks, but processes tasks in the queue.

· STOP: STOP At this point, the thread pool will not accept new tasks, nor will it process tasks in the queue, and the worker thread will be interrupted.

· TIDYING: Cleaning up. All tasks terminated and the number of threads terminated is 0, start calling terminated().

· TERMINATED: TERMINATED. Terminated () execution is complete.

The thread pool state changes as shown below.

2. Internal components of the thread pool

The interior of the thread pool mainly contains the following components. If you know what objects ThreadPoolExecutor contains, it’s pretty clear how thread pools work. Note that largestPoolSize, completedTaskCount, keepAliveTime, corePoolSize, and maximumPoolSize are not given in the table.

3. The thread pool submits the task execution logic

The previous article looked at the relationship between the number of threads in a thread pool and the blocking queue. Typically, the core thread is created first, then placed in the task queue, and then more threads are created, as shown in the figure below.

In fact, the commit method of thread pool ThreadPoolExecutor, execute(), executes according to these three phases. Submission consists of 3 steps: First, determine whether the number of threads is smaller than the number of core threads. If so, create a worker immediately and directly pass the task to the newly created worker (the task will not enter the blocking queue), start the worker thread immediately, and the function returns. Then, if the number of threads is greater than the number of core threads, it tries to put the task into the blocking queue. If it succeeds, it needs to check whether the thread pool is SHUTDOWN and whether the number of worker threads is zero again. If it becomes SHUTDOWN, the task that was just queued needs to be taken out of the queue (because of a sudden SHUTDOWN () call) and then processed according to the saturation policy (to handler). If the number of threads becomes zero, a new worker thread needs to be created (the core thread just checked just hangs, and if the worker is not created, the thread pool goes to rest). Finally, if putting into the blocking queue fails, a non-core worker thread is created to handle the task. If this too fails, the task is handed over to a saturation handler. The flow chart is as follows:

Note: The state of the thread pool and the number of threads must be re-evaluated between every two operations (with other operations in between) to keep ThreadPoolExecutor thread-safe. Thus, it is difficult to design thread-safe classes.

4. Execution logic of Worker threads

The thread pool uses worker threads to fetch tasks from the queue for execution. When the worker finishes a task, if the time slice is not used up and there are tasks in the task queue, the worker will continue to pick the task to execute, which can reduce unnecessary switching. So, we can guess that the Worker thread is roughly a loop. In fact, a worker is indeed a loop, and the body logic is constantly fetching tasks from the queue and executing them. In addition, the execution code of worker thread also contains some performance optimization measures and fault-tolerant logic. For example, the firstTask is placed directly in worker instead of queue (so worker checks whether its firstTask is not empty each time). After the task code throws an exception that causes the thread to terminate, a new thread is created to replace the thread that failed, etc. (see the code for the processWorkerExit method). According to the ThreadPoolExecutor. RunWorker code analysis, it is concluded that the worker’s execution logic as shown in the figure below:

To be clear, getTask blocks when the queue is empty; GetTask returns null when an operation such as shutdown() is called, at which point the worker thread determines whether a cleanup operation needs to be performed. The cleanup of the thread pool is done by one idle worker thread, and only one worker does the cleanup. Shutdown and shutdownNow only modify the state and issue notifications. Further, even if corePoolSize is set to 0 to allow core threads to exit, the thread pool reserves at least one thread for cleanup, and all threads exit only after the pool is closed.

5. How to judge whether worker threads are free, and how to distinguish core threads from non-core threads

Each worker is actually a lock (through inheritance AbstractQueuedSynchronizer), enclosed within the worker the worker thread. When the worker starts running, it will lock the worker and unlock the worker after the task is completed (this is to avoid the interruption signal of the thread pool affecting the task code, and the thread pool can wake up the worker threads blocked in getTask by sending the interruption to the worker). So, if the tryLock on the worker succeeds, the worker is free.

So where did keepAliveTime go? Actually, it’s in the getTask method. Each worker thread retrieves the task via a timed wait method poll(keepAliveTime, timeUnit.nanoseconds). When poll returns, the idle time exceeds the keepAliveTime time. So, how are core threads distinguished from non-core threads? In fact, worker threads in the thread pool are all peer, with no distinction between core and non-core threads. Each time a task is fetched from the queue, the number of current threads is determined. If the number of current threads is greater than corePoolSize, the timed version of poll is used to fetch the task. In this case, the worker thread is a non-core thread. If less than or equal to corePoolSize, the open-ended wait method take is used to fetch the task from the queue, which is the core thread.

6. Other details

Worker threads call beforeExecute() and afterExecute() before and after executing tasks.

Thread pool terminated() is called after it is closed.

To monitor thread pool status: getPoolSize(), getActiveCount(), getLargestPoolSize(), getTaskCount(), getCompletedTaskCount().

The last

In fact, thread pool can be improved. We do not know the execution time of each task, so each worker thread does not distinguish the size of runnable and generally executes tasks according to FIFO policy. Thread pool performance can be further optimized using a reasonable programming algorithm (dynamic programming) that maximizes the utilization of time slices, but this can be complex to design.