How thread pools work

We all use thread pools more or less in our work, but why use thread pools? From his name, we should know that the thread pool uses a pooling technology, like many other pooling technologies, for more efficient use of resources, such as link pools, memory pools, and so on.

Database link is a very expensive resource, and both creation and destruction need to pay a high price. In order to avoid frequent creation of database links, the link pool technology is produced. Create a batch of database links in the pool first. When you need to access the database, go directly to the pool to get an available link, and then return it to the link pool after use.

Similarly, threads are also a precious resource, and also a limited resource, and creating and destroying threads is equally costly. All of our code is supported by threads one by one, and today's chip architecture also determines that we must write programs that execute multi-threaded execution to obtain the highest program performance.

So how to efficiently manage the division of labor and cooperation between multiple threads has become a key issue. God Doug Lea designed and implemented a thread pool tool for us. Through this tool, we can realize the ability of multi-threading and realize the task Efficient execution and scheduling.

In order to use the thread pool tool correctly and reasonably, it is necessary for us to understand the principle of the thread pool.

This article mainly analyzes the thread pool from three aspects: thread pool status, important attributes, and workflow.

thread pool status

First of all, the thread pool is stateful. These states identify some operating conditions inside the thread pool. The process from opening to closing of the thread pool is a process of circulating the state of the thread pool.

There are five states of the thread pool:

thread-pool-executor-status.jpg

condition meaning
RUNNING Running state, in this state the thread pool can accept new tasks, and can also process tasks in the blocking queue<br />Execute the shutdown method to enter the SHUTDOWN state<br />Execute the shutdownNow method to enter the STOP state
SHUTDOWN To be closed, no longer accept new tasks, continue to process tasks in the blocking queue<br /> When the tasks in the blocking queue are empty and the number of worker threads is 0, enter the TIDYING state
STOP Stop state, do not receive new tasks, do not process tasks in the blocking queue, and will try to end the tasks in execution<br /> When the number of worker threads is 0, enter the TIDYING state
TIDYING Finishing the state, at this time the tasks have been executed, and there is no worker thread <br /> to enter the TERMINATED state after executing the terminated method
TERMINATED Terminated state, at which point the thread pool is completely terminated and the release of all resources is completed

important attributes

There are many core parameters of a thread pool, each of which has a special function. After all parameters are aggregated together, the complete work of the entire thread pool will be completed.

1. Thread status and number of worker threads

First of all, the thread pool is stateful. The behavior of the thread pool in different states is different. The five states have been mentioned above.

In addition, the thread pool definitely needs threads to perform specific tasks, so an inner class Worker is encapsulated in the thread pool as a worker thread, and each Worker maintains a Thread.

One of the key points of the thread pool is to control the rational and efficient use of thread resources, so the number of worker threads must be controlled, so it is necessary to save the number of worker threads in the current thread pool.

Seeing this, do you think you need to use two variables to save the state of the thread pool and the number of worker threads in the thread pool? But in ThreadPoolExecutor, only one variable of type AtomicInteger is used to save the values ​​of these two properties, that is ctl.

ctl.jpg

The upper 3 bits of ctl are used to indicate the state of the thread pool (runState), and the lower 29 bits are used to indicate the number of worker threads (workerCnt). Why use 3 bits to indicate the status of the thread pool? The reason is that the thread pool has a total of 5 states, and 2 bits can only represent 4 situations, so at least 3 bits are needed to represent 5 states.

2. The number of core threads and the maximum number of threads

Now that there is a variable that marks the number of worker threads, how many threads should there be? Too many threads waste thread resources, and if there are too few threads, the performance of the thread pool cannot be exerted.

In order to solve this problem, the thread pool has designed two variables to cooperate, namely:

  • Number of core threads: corePoolSize is used to represent the number of core threads in the thread pool, also known as the number of idle threads
  • Maximum number of threads: maximumPoolSize is used to indicate the maximum number of threads that can be created in the thread pool

Now we have a question. Since there are already variables that identify the number of worker threads, why do we need to have the number of core threads and the maximum number of threads?

In fact, if you think about it this way, you can understand that creating a thread has a cost. You cannot create a thread every time you want to perform a task, but you can’t when there are too many tasks, only a small number of threads are executing, so the task is too late. Instead, it should create appropriate enough threads to process tasks in a timely manner. With the change of the number of tasks, when the number of tasks is obviously very small, there is no need for the redundant threads originally created to survive, because at this time a small number of threads can be used to process them, so the number of really working threads , which varies with the task.

What is the relationship between the number of core threads and the maximum number of threads and the number of worker threads?

core-maximum-pool-size.jpg

The number of worker threads may vary from 0 to the maximum number of threads, and may remain at corePoolSize after a period of execution, but it is not absolute, depending on whether the core threads are allowed to be reclaimed by timeout.

3. The factory that creates the thread

Since it is a thread pool, threads are naturally indispensable. How to create threads? This task is handed over to the thread factory ThreadFactory to complete.

4. Blocking queue of cache tasks

Above we talked about the number of core threads and the maximum number of threads, and also introduced that the number of worker threads varies between 0 and the maximum number of threads. But it is impossible to create all threads at once and fill the thread pool, but there is a process, which is as follows:

When the thread pool receives a task, if the number of worker threads does not reach corePoolSize, a new thread will be created and the task will be bound, and the previous thread will not be reused until the number of worker threads reaches corePoolSize.

When the number of worker threads reaches corePoolSize and a new task is received, the task will be stored in a blocking queue for the core thread to execute. Why not just create more threads to perform new tasks, the reason is that in the core thread it is likely that some threads have already completed their tasks, or there are other threads that can finish the current task immediately, and can then invest Go to new tasks, so the blocking queue is a buffering mechanism, giving core threads a chance to let them fully utilize their capabilities. Another reason worth considering is that, after all, creating threads is relatively expensive, and it is impossible to create a new thread as soon as there is a task to perform.

So we need to equip the thread pool with a blocking queue to temporarily cache tasks that will wait for worker threads to execute.

work-queue.jpg

5. Non-core thread survival time

We said above that when the number of worker threads reaches corePoolSize, the thread pool will store the newly received tasks in the blocking queue, and the blocking queue has two situations: one is a bounded queue and the other is an unbounded queue.

If it is an unbounded queue, then when the core threads are busy, all newly submitted tasks will be stored in the unbounded queue. At this time, the maximum number of threads will become meaningless, because the blocking queue will not be full. condition.

If it is a bounded queue, then when the blocking queue is full of tasks waiting to be executed, and when a new task is submitted, the thread pool needs to create a new "temporary" thread to process it, which is equivalent to dispatching additional manpower to process the task. .

However, the created "temporary" threads have a survival time, and it is impossible to keep them alive all the time. When the tasks in the blocking queue are executed and there are not so many new tasks submitted, the "temporary" threads need to be Recycling and destruction, the period of time waiting before being recycled and destroyed is the survival time of non-core threads, which is the keepAliveTime attribute.

So what is a "non-core thread"? Is the thread created first is the core thread, and the thread created later is the non-core thread?

In fact, the core thread has nothing to do with the order of creation, but with the number of worker threads. If the current number of worker threads is greater than the number of core threads, then all threads may be "non-core threads", all of which have been recycled possible.

After a thread finishes executing a task, it will fetch a new task from the blocking queue. It is an idle thread until the task is fetched.

There are two ways to take a task, one is to block until the task is taken out through the take() method, and the other is to take out the task within a certain period of time or time out through the poll(keepAliveTime, timeUnit) method. If it times out, the thread will be blocked. Recycling, please note that core threads are generally not recycled.

So how to ensure that the core thread will not be recycled? It is still related to the number of worker threads. When each thread takes a task, the thread pool will compare the current number of worker threads with the number of core threads:

  • If the number of worker threads is less than the current number of core threads, the first method is used to fetch tasks, that is, there is no timeout for recycling. At this time, all worker threads are "core threads", and they will not be recycled;
  • If it is greater than the number of core threads, the second method is used to fetch the task, and once it times out, it will be recycled, so there is no absolute core thread, as long as the thread does not fetch the task to execute within the survival time, it will be recycled.

Therefore, if each thread wants to keep its "core thread" identity, it must make full efforts to obtain tasks to execute as quickly as possible, so as to avoid the fate of being recycled.

Core threads are generally not recycled, but it is not absolute. If we set to allow core threads to be recycled over time, then there is no such thing as core threads. All threads will obtain tasks through poll(keepAliveTime, timeUnit). , once the task cannot be acquired over time, it will be recycled. Generally, it is rarely used in this way, unless the thread pool needs to process very few tasks and the frequency is not high, so there is no need to maintain the core thread all the time.

6. Rejection Policy

Although we have a blocking queue to cache tasks, which provides a buffer period for the execution of the thread pool to a certain extent, but if it is a bounded blocking queue, there is a situation where the queue is full, and there is also data for worker threads. When the maximum number of threads has been reached. If a new task is submitted at this time, it is obvious that the thread pool has more than enough resources, because there is neither free queue space to store the task, nor can a new thread be created to execute the task, so at this time we There needs to be a rejection strategy, that is, handler.

The rejection policy is a variable of type RejectedExecutionHandler. Users can specify the rejection policy by themselves. If not specified, the thread pool will use the default rejection policy: throw an exception.

There are many other rejection strategies that we can choose from in the thread pool:

  • just drop the task
  • Execute the task using the caller thread
  • Discard the oldest task in the task queue, then submit the task

work process

After understanding all the important properties in the thread pool, now we need to understand the workflow of the thread pool.

how-thread-pool-work.jpg

The above picture is a simplified picture of the thread pool work. The actual process is much more complicated than this, but these should be able to completely cover the entire workflow of the thread pool.

The whole process can be divided into the following parts:

1. Submit the task

When submitting a new task to the thread pool, the thread pool has three processing conditions, namely: creating a worker thread to execute the task, adding the task to the blocking queue, and rejecting the task.

The process of submitting tasks can also be split into the following parts:

  • When the number of worker threads is less than the number of core threads, a new core worker thread is created directly
  • When the number of worker threads is not less than the number of core threads, you need to try to add tasks to the blocking queue
  • If it can be successfully added, it means that the queue is not full, then the following secondary verification needs to be done to ensure that the added tasks can be successfully executed
    • Verify the running state of the current thread pool. If it is in a non-RUNNING state, you need to remove the task from the blocking queue and then reject the task
    • Verify the number of worker threads in the current thread pool. If it is 0, you need to actively add an empty worker thread to execute the task just added to the blocking queue
  • If the join fails, the queue is full, then a new "temporary" worker thread needs to be created to execute the task
    • If the creation is successful, execute the task directly
    • If the creation fails, it means that the number of worker threads is equal to the maximum number of threads, and the task can only be rejected

The whole process can be represented by the following picture:

execute-runnable.jpg

2. Create a worker thread

Creating a worker thread requires a series of judgments. It is necessary to ensure that the current thread pool can create a new thread before it can be created.

First, when the state of the thread pool is SHUTDOWN or STOP, new threads cannot be created.

In addition, when the thread factory fails to create a thread, it cannot create a new thread.

In addition, the number of current worker threads is compared with the number of core threads and the maximum number of threads. If the former is greater than the latter, it is not allowed to create.

In addition, it will try to increase the number of worker threads through CAS. If the self-increment is successful, a new worker thread, that is, a Worker object, will be created.

Then lock for a second time to verify whether a worker thread can be created, and finally if the creation is successful, the worker thread will be started.

3. Start the worker thread

When the worker thread is successfully created, that is, the Worker object has been created, then you need to start the worker thread and let the thread start working. There is a Thread associated with the Worker object, so if you want to start the worker thread, you only need to pass the worker .thread.start() to start the thread.

After the startup, the run method of the Worker object will be executed. Because the Worker implements the Runnable interface, the Worker is essentially a thread.

After the thread start is opened, the run method of the Runnable will be called. In the run method of the worker object, the runWorker(this) method is called, that is, the current object is passed to the runWorker method for execution.

4. Get the task and execute it

After the runWorker method is called, it is to execute a specific task. First, you need to get an executable task, and a task is bound to the Worker object by default. If the task is not empty, it is directly executed.

After the execution is completed, it will go to the blocking queue to obtain the task for execution, and the process of obtaining the task needs to consider the number of current worker threads.

  • If the number of worker threads is greater than the number of core threads, then it needs to be obtained through poll, because at this time the idle threads need to be recycled;
  • If the number of worker threads is less than or equal to the number of core threads, it can be obtained through take, so all threads are core threads at this time, and do not need to be recycled, provided that allowCoreThreadTimeOut is not set

Houyi is code-by-code, focusing on original sharing, describing the source code and principles with easy-to-understand pictures and texts

{{o.name}}
{{m.name}}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=324125121&siteId=291194637