7000 words + 24 pictures take you to understand the thread pool thoroughly

Today, I will talk with you about thread pools that are commonly used at work or frequently asked in interviews. By drawing pictures, I will thoroughly understand the working principle of thread pools, and how to customize thread pools suitable for business in actual projects.

1. What is a thread pool

The thread pool is actually an implementation of a pooling technology. The core idea of ​​the pooling technology is to realize the reuse of resources and avoid the performance overhead caused by the repeated creation and destruction of resources. In the thread pool, the thread pool can manage a bunch of threads, so that the threads will not be destroyed after performing tasks, but continue to process tasks that have been submitted by other threads.

The benefits of thread pools:

  • Reduce resource consumption. Reduce the cost of thread creation and destruction by reusing already created threads.

  • Improve responsiveness. When a task arrives, the task can be executed immediately without waiting for the thread to be created.

  • Improve thread manageability. Threads are scarce resources. If they are created without restrictions, it will not only consume system resources, but also reduce the stability of the system. Using thread pools can be used for unified allocation, tuning, and monitoring.

Second, the construction of the thread pool

In Java, thread pools are mainly created by building ThreadPoolExecutor. Next, let's take a look at how thread pools are constructed.

Thread pool construction parameters

  • corePoolSize: The number of core threads in the thread pool for work.

  • maximumPoolSize: The maximum number of threads, the maximum number of threads allowed to be created by the thread pool.

  • keepAliveTime: The survival time of threads created after corePoolSize is exceeded or the maximum survival time of all threads, depending on the configuration.

  • unit: The time unit of keepAliveTime.

  • workQueue: task queue, which is a blocking queue. When the number of threads has reached the number of core threads, the task will be stored in the blocking queue.

  • threadFactory : The factory used to create threads inside the thread pool.

  • handler: Rejection policy; when the queue is full and the number of threads reaches the maximum number of threads, this method is called to process the task.

The construction of the thread pool is actually very simple, that is, pass in a bunch of parameters, and then perform a simple assignment operation.

Third, the operation principle of the thread pool

After talking about the meaning of the core construction parameters of the thread pool, let's draw a picture to explain how these parameters work in the thread pool.

What does the thread pool look like when it is just created, as shown below

Yes, there is only one blocking queue passed in during the construction in the newly created thread pool. At this time, there are no threads in it, but if you want to have the number of core threads created before execution, you can call the prestartAllCoreThreads method to achieve this. , the default is no thread.

What happens when a thread submits a task via the execute method?

When submitting a task, it will actually process the task

First, it will determine whether the number of threads in the current thread pool is less than the number of core threads, that is, the parameter corePoolSize passed in when the thread pool is constructed.

If it is less than, then create a thread directly through ThreadFactory to perform this task, as shown in the figure

When the task is executed, the thread will not exit, but will get the task from the blocking queue, as shown below

Next, if another task is submitted, it will also follow the above steps to determine whether it is less than the number of core threads. If it is less than the number of core threads, a thread will still be created to execute the task, and the task will be obtained from the blocking queue after execution. There is a detail here, that is, when submitting a task, even if there are threads in the thread pool that cannot obtain tasks from the blocking queue, if the number of threads in the thread pool is still less than the number of core threads, threads will continue to be created instead of Use an existing thread.

What if the number of threads in the thread pool is no longer less than the number of core threads? Then at this time, it will try to put the task into the blocking queue. After the queue is successful, as shown in the figure

In this way, the blocked thread can get the task.

However, as there are more and more tasks, the queue is full, and the task placement fails, what should I do?

At this point, it will be judged whether the number of threads in the current thread pool is less than the maximum number of threads, that is, the maximumPoolSize parameter when entering the parameter

If it is less than the maximum number of threads, non-core threads will also be created to execute the submitted tasks, as shown in the figure

Therefore, it can be found from this that even if there are tasks in the queue, the newly created thread will give priority to processing the submitted task instead of obtaining the existing tasks from the queue for execution. implement.

But unfortunately, the number of threads has reached the maximum number of threads, so what will happen at this time?

At this point, the rejection policy will be executed, that is, when the thread pool is constructed, the incoming RejectedExecutionHandler object will be used to process this task.

Implementation of RejectedExecutionHandler There are 4 defaults that come with JDK

  • AbortPolicy: Abort task, throw runtime exception

  • CallerRunsPolicy: The task is executed by the thread that submitted the task

  • DiscardPolicy: Discard this task without throwing an exception

  • DiscardOldestPolicy: Remove the task that entered the queue first from the queue, and then submit the task again

When the thread pool is created, if the deny policy is not specified, the default policy is AbortPolicy. Of course, you can also implement the RejectedExecutionHandler interface yourself, such as storing tasks in the database or cache, so that the rejected tasks can be obtained from the database or cache.

At this point, we found that several parameters corePoolSize, maximumPoolSize, workQueue, threadFactory, and handler of the thread pool construction have been mentioned in the above execution process, so there are still two parameters keepAliveTime and unit (unit is the time unit of keepAliveTime) I didn't mention it, so how does keepAliveTime work? This question is left for later analysis.

Talking about the entire execution process, let's see how the execute method code is implemented.

execute method

  • workerCountOf(c)<corePoolSize: This line of code is to determine whether it is less than the number of core threads. If so, use the addWorker method. addWorker is to add threads to perform tasks.

  • workQueue.offer(command): This line of code means trying to add a task to the blocking queue

  • After the addition fails, the addWorker method will be called again to try to add a non-core thread to execute the task

  • If adding a non-core thread still fails, reject(command) will be called to reject the task.

Finally, draw another picture to summarize the execution process

Fourth, the principle of reuse of threads in the thread pool

The core function of the thread pool is to realize the reuse of threads, so how does the thread pool realize the reuse of threads?

The thread is actually encapsulated as a Worker object inside the thread pool

Worker inherits AQS, that is, it has the characteristics of certain locks.

The method of creating a thread to perform a task mentioned above is created by the addWorker method. When creating a Worker object, the thread and task are encapsulated into the Worker together, and then the runWorker method is called to let the thread execute the task. Next, let's take a look at the runWorker method.

Start thread processing tasks

From this picture, we can see the reason why the thread will not exit after executing the task. The while infinite loop is used inside the runWorker. After the first task is executed, the task will be continuously obtained through the getTask method. As long as the task can be obtained, it will be The run method will be called to continue executing the task, which is the main reason why threads can be reused.

However, if the method cannot be obtained from getTask, the processWorkerExit method in finally will be called to exit the thread.

One of the details here is that because Worker inherits AQS, it will call the lock method of Worker every time before executing the task. After executing the task, it will call the unlock method. The purpose of this can be done through the locked state of the Worker. Determine whether the current thread is running a task. If you want to know whether the thread is running a task, you only need to call the tryLock method of Worker, and you can judge according to whether the lock is successful. If the lock is successful, it means that the current thread is not locked, and there is no task to execute. Call the shutdown method to close the thread pool. When , use this method to determine whether the thread is executing a task, and if not, try to interrupt the thread that is not executing the task.

5. How threads get tasks and how to implement timeouts

In the previous section, we said that after the thread finishes executing the task, it will continue to obtain the task from the getTask method, and it will exit if it is not obtained. Next, let's take a look at the implementation of the getTask method.

getTask method

The getTask method, in front of which is the judgment of some states of the thread pool, here is a line of code

boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

This line of code is to determine whether the thread that is currently coming to get the task can exit over time. If allowCoreThreadTimeOut is set to true or the current number of threads in the thread pool is greater than the number of core threads, that is, corePoolSize, then the thread that acquires the task can exit over time.

That is how to achieve timeout and exit, it is this line of core code

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

The poll method or the take method of the blocking queue workQueue will be called according to whether the timeout is allowed. If the timeout is allowed, the poll method will be called, and keepAliveTime is passed in, which is the idle time passed in when the thread pool is constructed. This method means to block the keepAliveTime time from the queue to obtain the task. If it cannot be obtained, it will return null; if If the timeout is not allowed, the take method will be called, which will block the acquisition of the task until the task position is obtained from the queue. From here you can see how keepAliveTime is used.

So here you should know why the threads in the thread pool can be idle for a certain period of time and then exit. In fact, the most important thing is to use the implementation of the poll method of the blocking queue. This method can specify the timeout time. Once the thread reaches the keepAliveTime and has not obtained the task, it will return null. As mentioned in the previous section, the getTask method returns null. The thread will exit.

There is also a detail here, that is, when judging whether the thread currently acquiring the task can exit overtime, if allowCoreThreadTimeOut is set to true, then all threads reach this timed is true, then all threads, including core threads, can achieve timeout quit. If your thread pool needs to time out core threads, you can set the allowCoreThreadTimeOut variable to true through the allowCoreThreadTimeOut method.

The entire getTask method and the mechanism for thread timeout and exit are shown in the figure

6. Five states of the thread pool

There are 5 constants inside the thread pool to represent the five states of the thread pool

  • RUNNING: The thread pool is in this state when it is created and can receive new tasks and process added tasks.

  • SHUTDOWN: The thread pool will be converted to the SHUTDOWN state by calling the shutdown method. At this time, the thread pool will no longer receive new tasks, but can continue to process the tasks that have been added to the queue.

  • STOP: When the shutdownNow method is called, the thread pool will switch to the STOP state, will not receive new tasks, and will not be able to continue processing the tasks that have been added to the queue, and will try to interrupt the threads of the tasks being processed.

  • TIDYING: In the SHUTDOWN state, the number of tasks is 0, all other tasks have been terminated, and the thread pool will become TIDYING state. When the thread pool is in the SHUTDOWN state, the task queue is empty and the tasks being executed are empty, the thread pool will change to the TIDYING state. When the thread pool is in the STOP state and the executing tasks in the thread pool are empty, the thread pool will change to the TIDYING state.

  • TERMINATED: The thread pool is completely terminated. After the thread pool executes the terminated() method in the TIDYING state, it will change to the TERMINATED state.

The thread pool state is stored in the ctl member variable, which not only stores the state of the thread pool but also stores the size of the number of threads in the current thread pool.

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

Finally, draw a picture to summarize the flow of these five states

In fact, during the running process of the thread pool, before most operations are performed, it is necessary to determine which state the current thread pool is in, and then decide whether to continue the operation.

7. Closing the thread pool

The thread pool provides shutdown and shutdownNow methods to close the thread pool.

shutdown method

It is to change the state of the thread pool to SHUTDOWN, and then try to interrupt the idle thread (how to judge the idle, as mentioned above when the Worker inherits AQS), that is, blocking the thread waiting for the task.

shutdownNow method

It is to change the state of the thread pool to STOP, and then try to interrupt all threads and remove the remaining tasks from the blocking queue, which is why shutdownNow cannot execute the remaining tasks.

Therefore, it can also be seen that the main difference between the shutdown method and the shutdownNow method is that the tasks in the queue can be processed after shutdown. The shutdownNow method directly removes the task from the queue, and the threads in the thread pool are no longer processed.

Eight, thread pool monitoring

When using a thread pool in a project, it is generally necessary to monitor the thread pool so that it can be checked when a problem occurs. The thread pool itself provides some methods to get the running status of the thread pool.

  • getCompletedTaskCount: The number of completed tasks

  • getLargestPoolSize: The maximum number of threads ever created in the thread pool. This is mainly used to determine whether the thread is full.

  • getActiveCount: Get the thread data of the executing task

  • getPoolSize: Get the size of the number of threads in the current thread pool

In addition to the above implemented methods provided by the thread pool, the thread pool also reserves many extension methods. For example, in the runWorker method, the beforeExecute method is called back before the task is executed, and the afterExecute method is called back after the task is executed. These methods are empty implementations by default. You can extend and rewrite these methods by inheriting ThreadPoolExecutor to achieve what you want. Function.

Nine, Executors build thread pool and problem analysis

JDK provides Executors, a tool class, to quickly create thread pools.

Thread pool with fixed number of threads: the number of core threads is equal to the maximum number of threads

Thread pool for the number of individual threads

Thread pool with near infinite number of threads

Thread pool with timing scheduling function

Although JDK provides a method to quickly create a thread pool, it is not recommended to use Executors to create a thread pool, because it can be seen from the above construction of the thread pool that the newFixedThreadPool thread pool, due to the use of LinkedBlockingQueue, the capacity of the queue is infinite by default. When there are too many tasks in use, it will cause memory overflow; because the number of core threads in the newCachedThreadPool thread pool is infinite, when there are too many tasks, a large number of threads will be created, and the machine load may be too high, which may cause service downtime.

10. Usage scenarios of thread pools

In java programs, it is often necessary to use multithreading to process some business, but it is not recommended to simply inherit Thread or implement the Runnable interface to create threads, which will lead to frequent creation and destruction of threads, and too many threads are created at the same time There may also be a risk of resource exhaustion. Therefore, in this case, using a thread pool is a more reasonable choice, which is convenient for managing tasks and realizing the reuse of threads. Therefore, thread pools are generally suitable for scenarios that require asynchronous or multi-threaded processing tasks.

11. How to reasonably customize the thread pool in the actual project

Through the above analysis, it is mentioned that the thread pool created by the tool class Executors cannot meet the actual usage scenarios. So in the actual project, how to construct the thread pool and how to set the parameters reasonably?

1) Number of threads

The setting of the number of threads mainly depends on whether the business is IO-intensive or CPU-intensive.

CPU-intensive means that the task is mainly used to do a lot of computation, and nothing causes the thread to block. Generally, the number of threads in this scenario is set to the number of CPU cores + 1.

IO-intensive: When executing tasks that require a lot of IO, such as disk io and network io, there may be a lot of blocking, so using multithreading in IO-intensive tasks can greatly speed up the processing of tasks. The general number of threads is set to 2 * the number of CPU cores

The method used to get the number of CPU cores in java is:

Runtime.getRuntime().availableProcessors();

2) Thread Factory

It is generally recommended to customize the thread factory and set the thread name when building a thread, so that it is easy to know which thread executes the code when checking the log.

3) Bounded Queue

Generally, it is necessary to set the size of the bounded queue. For example, when LinkedBlockingQueue is constructed, parameters can be passed in to limit the size of the task data in the queue, so that the system will not be oom caused by infinitely throwing tasks into the queue.

Guess you like

Origin blog.csdn.net/m0_71777195/article/details/127035268