The thread pool
Thread pool details
- Parameter meaning
2. Thread creation timing
- If we have a task that is so special that we have reached the maximum capacity of the workQueue, then the thread pool will start the backup force, which is the maxPoolsize maximum number of threads, and the thread pool will continue to create threads to execute the task based on the corePoolsize number of core threads
- Assuming that tasks are continuously submitted, the thread pool continues to create threads until the number of threads reaches maxPoolsize
- If a task is still being submitted, exceeding the maximum capacity of the thread pool, the thread pool will reject the task
- As you can see, the thread pool will reject the task if corePoolsize, workQueue, and maxPoolsize are still insufficient
- CorePoolsize refers to the number of core threads. When the thread pool is initialized, the default number of threads is 0. When a new task is submitted, a new thread is created to execute the task Poolsizes, because they are core threads, are not destroyed even if there may be no future tasks to execute
- As the number of tasks increases, after the task queue is full, the thread pool will create further threads, up to maxPoolsize to cope with the scenario of multiple tasks. If there are idle threads in the future, threads larger than corePoolsize will be properly reclaimed
- So normally, the number of threads in the thread pool will be in the closed range between corePoolSize and maxPoolsize
- Thread pool characteristics
- Thread pools want to keep the number of threads small and add threads only when the load becomes large
- The thread pool creates more threads than corePoolsize only when the task queue is full. If you use an unbounded queue (such as LinkedBlockingQueue), the number of threads does not exceed corePoolsize because the queue is never full
- By setting corePoolsize and maxPoolsize to the same value, you can create a thread pool of fixed size
- By setting maxPoolsize to a high value, such as nteger.max_value, you can allow the thread pool to create as many threads as you want
- Parameters,
-
KeepAliveTime Time unit
When the number of threads in thread pool than in the core number of threads, while there is no task to do, the thread pool threads will detect the keepAliveTime if in excess of the prescribed time, nothing to do the thread is destroyed, in order to reduce memory footprint and resource consumption if late tasks and more up, the thread pool will be according to the rules to create a thread, so it SetKeepAliveTime () is a scalable and flexible procedure. We can also dynamically change the value of keepAliveTime using the setKeepAliveTime() method
-
ThreadFactory
ThreadFactory is actually a thread factory, its role is to produce the thread in order to perform a task we can choose to use the default thread factory, create threads will be in the same thread group and has the same priority, and is not a daemon thread we can also choose their own custom thread factory, for easy to thread the custom naming different thread pool The threads in the thread will usually have different custom thread names based on the specific business
Thread rejection policy
Create a new thread pool, use an ArrayBlockingQueue with a maximum capacity of 10 as the task queue, and specify a core number of threads in the thread pool of 5 and a maximum number of threads of 10. Assume that 20 time-consuming tasks have been submitted. In this case, the thread pool will first create a core number of threads, i.e. 5 threads, to execute the task. The queue is then filled with 10 tasks, and new threads continue to be created until the maximum number of threads is 10. The thread pool a total of 20 tasks, including 10 tasks are being 10 thread execution, and ten tasks in the task queue waiting, and due to the largest number of threads to the thread pool is 10, so can’t afford to add more thread processing tasks, with the help of this means that when a thread pool saturation, work again at this time will be upon the presentation of the new task Refused to
Java provides four default rejection policies for different scenarios in the ThreadPoolExecutor class, all of which implement the RejectedExecutionHandler interface, as shown in the figure below:
- AbortPolicy
The first refused to strategy is AbortPolicy, such rejection policies in refused to task, can throw a type directly for RejectedExecutionException RuntimeException, let you perceive task was rejected, Depending on the business logic, you can either retry or abandon the commit strategy
- DiscardPolicy
The second rejection policy is DiscardPolicy. As its name describes, when a new task is submitted, it will be directly discarded without giving you any notice. Relatively speaking, there is a certain risk, because we do not know that the task will be discarded when we submit it, which may cause data loss
- DiscardOldestPolicy
DiscardOldestPolicy discards the queue with the longest lifetime if the thread pool is not closed. DiscardOldestPolicy discards the queue with the longest lifetime if the thread pool is not closed. This frees up space for newly committed tasks, but there is also a risk of data loss
- CallerRunsPolicy
The fourth rejection policy is CallerRunsPolicy, which is relatively complete. When a new task is submitted, if the thread pool is not closed and cannot execute it, the task is handed over to the thread that submitted the task, that is, whoever submits the task is responsible for executing the task. There are two main advantages to this
1). First, newly submitted tasks are not discarded, so there is no loss of business
2). The second advantage is that because who submit task who will be responsible for performing the task, so submit task thread responsible for performing tasks, and perform the task is more time consuming, during this period, submit task thread is occupied, also won’t submit new tasks, slow down the speed of tasks submission, rather then a negative feedback. During this period, threads in the thread pool can also make full use of this period to execute some tasks, freeing up some space, which is equivalent to giving the thread pool a certain buffer period
Thread pool benefits
The benefits of using thread pools
There are three main advantages to using a thread pool over manually creating threads.
First, thread pools can address the overhead of the thread life cycle while also speeding up response times. Because threads in the thread pool are reusable, we use only a few threads to perform a large number of tasks, which greatly reduces the overhead of the thread life cycle. Threads are usually not created AD hoc, but are already created and ready to execute tasks, which eliminates thread creation delays, improves response times, and enhances the user experience.
Second, thread pools can coordinate memory and CPU usage to avoid resource misuse. The thread pool flexibly controls the number of threads based on the configuration and number of tasks, creating when there are not enough threads and recycling when there are too many, avoiding memory overflow due to too many threads or WASTING CPU resources due to too few threads. It strikes a perfect balance.
Third, thread pools can centrally manage resources. For example, thread pool can unify the management of task queue and thread, and can uniformly start or end tasks, which is more convenient and easier to manage than a single thread to process tasks one by one. At the same time, it is also conducive to data statistics, for example, we can easily count the number of tasks that have been executed.
Six common thread pools
- FixedThreadPool
The first type of thread pool is called FixedThreadPool. The number of core threads is the same as the maximum number of threads, so it can be regarded as a fixed number of threads pool. The feature of this pool is that the number of threads in the pool increases from zero to a fixed number of threads. Instead of creating more threads to process tasks, the thread pool puts tasks that exceed the thread’s capacity into a task queue to wait. And even if the task queue is full, when the number of threads should be increased, the maximum number of threads is the same as the number of core threads, so no more threads can be added.
- CachedThreadPool
The second type of thread pool is the CachedThreadPool, which can be called a cacheable thread pool. It is characterized by the fact that the number of threads can be increased almost indefinitely (the actual maximum can be Integer.MAX_VALUE, which is 2^31-1, which is so large that it is almost impossible to achieve). Threads can also be reclaimed when they are idle. That is, the number of threads in the thread pool is not fixed. Of course, there is a SynchronousQueue for submitting tasks, but this queue has a capacity of 0, and it does not actually store any tasks. It is only responsible for forwarding and delivering tasks, so it is efficient.
- ScheduledThreadPool
The third thread pool is the ScheduledThreadPool, which enables the scheduled or periodic execution of tasks. For example, every 10 seconds, there are three main ways to do this, as shown in the code:
ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
service.schedule(new Task(), 10, TimeUnit.SECONDS);
service.scheduleAtFixedRate(new Task(), 10.10, TimeUnit.SECONDS);
service.scheduleWithFixedDelay(new Task(), 10.10, TimeUnit.SECONDS);
Copy the code
So what’s the difference between these three methods?
1. The first method schedule is relatively simple, indicating that the task will be executed after the specified time. If the parameter is set to 10 seconds in the code, that is, the task will be executed after 10 seconds.
2. ScheduleAtFixedRate indicates that tasks are executed at a fixed frequency. The second parameter initialDelay indicates the time of the first delay, and the third parameter Period indicates the period, that is, how long it takes to execute a task after the first delay.
3. The third scheduleWithFixedDelay method is similar to the second method, which also executes tasks periodically. The difference lies in the definition of the period. Regardless of how long the task takes to execute; The scheduleWithFixedDelay method starts with the end of the task as the start of the next cycle.
- SingleThreadExecutor
The fourth thread pool is SingleThreadExecutor, which uses a single thread to execute a task. This works like a FixedThreadPool except that there is only one thread. If an exception occurs during the execution of a task, The thread pool also recreates a thread to perform subsequent tasks. Since there is only one thread, this type of thread pool is ideal for scenarios where all tasks need to be executed in the order in which they are submitted. The first few thread pools do not necessarily guarantee that the order in which the tasks are executed is equal to the order in which they are submitted because they are executed in parallel by multiple threads
- SingleThreadScheduledExecutor
The fifth thread pool is SingleThreadScheduledExecutor, it actually and third ScheduledThreadPool thread pool is very similar, it is just a special case of ScheduledThreadPool, within only a single thread, as shown in the source code:
new ScheduledThreadPoolExecutor(1)
Copy the code
It simply sets the number of core threads in the ScheduledThreadPool to 1.
To summarize the five thread pools mentioned above, we compare the number of core threads, the maximum number of threads, and the thread lifetime, as shown in the table
- ForkJoinPool
This thread pool was added in JDK 7. The name ForkJoin describes the execution mechanism. The main use of this thread pool is the same as that of the previous thread pool, which also has a queue to hold tasks. But there are two very big differences between the ForkJoinPool thread pool and its predecessors. The first is that it’s great for tasks that can generate subtasks.
We have a Task that can generate three sub-tasks, and the results are summarized to Result after the parallel execution of the three sub-tasks. For example, the main Task needs to perform a very heavy computing Task, so we can divide the computing into three parts, which are independent of each other. This allows you to take advantage of the CPU’s multiple cores, do parallel calculations, and then aggregate the results. There are two main steps involved. The first step is to Fork and the second step is to Join. By now you should know the name ForkJoinPool.
Print the values of terms 0 through 9 of the Fibonacci sequence:
package com.edu;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
/ * * *@versionThe 2020-10-08 * 1.0@auther<a href="mailto:[email protected]"> Betelgeuse </a> *@description
* @since1.0 * /
public class Fibonacci extends RecursiveTask<Integer> {
int n;
public Fibonacci(int n) {
this.n = n;
}
@Override
protected Integer compute(a) {
if (n <= 1) {
return n;
}
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
f2.fork();
return f1.join() + f2.join();
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
for (int i = 0; i < 10; i++) {
ForkJoinTask task = forkJoinPool.submit(newFibonacci(i)); System.out.println(task.get()); }}}Copy the code
The second difference lies in the internal structure. Whereas in the previous thread pool all threads share a single queue, ForkJoinPool has its own queue for each thread, as shown in the figure
In addition to a common task queue, each thread in the ForkJoinPool thread pool has a double-ended queue deque. When a task is forked, the subtasks are placed in the thread’s own deque rather than in the common queue. If the three subtasks in the thread of the t1, deque the queue for thread t1 task cost was reduced, can be directly in their own tasks in the queue for without having to go public in the queue for wouldn’t be happening behind (in addition to talk about steal case), reduce the competition and to switch between the threads, It’s very efficient
Internal structure of the thread pool
The internal structure of the thread pool is mainly composed of four parts, as shown in the figure.
-
The first part is the thread pool manager, which is mainly responsible for managing the creation, destruction, adding tasks and other management operations of the thread pool. It is the manager of the whole thread pool.
-
The second part is worker threads, threads T0 to T9 in the figure, which diligently fetch tasks from the task queue and execute them.
-
The third part is the task queue, as a kind of buffer mechanism, the thread pool will put the no processing task in the task queue, because get task from the task queue multiple threads is concurrent scenarios, task queue meet the requirements of thread safety is required at this time, so the thread pool task queue BlockingQueue was employed to guarantee thread safety.
-
The fourth part is tasks, which require the implementation of a unified interface so that worker threads can process and execute.
Blocking queue
Of the four main components of the thread pool, the blocking queue is of particular interest. As shown in the table, different thread pools use different blocking queues.
- LinkedBlockingQueue
For FixedThreadPool and SingleThreadExector, the blocking queue they use is LinkedBlockingQueue with capacity integer.max_value, which can be considered unbounded. Since the FixedThreadPool thread pool has a fixed number of threads, there is no way to add a particularly large number of threads to process tasks, so you need a LinkedBlockingQueue that has no capacity limit to hold tasks. Note that since the task queue of the thread pool is never full, the thread pool will only create threads with the number of core threads, so the maximum number of threads at this point is meaningless to the thread pool because it will not trigger the generation of more threads than the number of core threads.
- SynchronousQueue
The second blocking queue is SynchronousQueue, and the corresponding thread pool is CachedThreadPool. The maximum number of threads in the CachedThreadPool is Integer. A CachedThreadPool is the opposite of a FixedThreadPool, where the blocking queue has an infinite capacity, whereas a CachedThreadPool has an infinite number of threads that can be expanded indefinitely. So the CachedThreadPool thread pool does not require a task queue to store tasks, because once tasks are submitted, they are forwarded directly to the thread or new threads are created to execute them without saving them separately.
When we create our own thread pool using SynchronousQueue, we need to set the maximum number of threads as large as possible so that when the number of tasks exceeds the maximum number of threads, there is no way to queue the tasks and there are not enough threads to execute them.
- DelayedWorkQueue
The third kind of blocking queue is DelayedWorkQueue, its corresponding thread pool is ScheduledThreadPool and SingleThreadScheduledExecutor respectively, the biggest characteristic of these two kinds of thread pool is can delay the task, Such as performing tasks after a certain period of time or at regular intervals. DelayedWorkQueue is characterized by the fact that the internal elements are not sorted by the time they were put in, but by the amount of time they were delayed, using a “heap” data structure. Thread pool ScheduledThreadPool and SingleThreadScheduledExecutor DelayedWorkQueue choice, because they are based on time to perform a task, and delay queue just can take the task is sorted according to time, Facilitate the execution of tasks.
risk
Should not automatically create a thread pool, the so-called automatically create a thread pool is called direct Executors of various methods to generate learned in front of the common thread pool, such as Executors. NewCachedThreadPool (). But there are risks to doing so, so let’s take a look at some of the problems that can arise with automatic thread pool creation.
- FixedThreadPool
It is a thread pool with a fixed number of threads. As shown in the source code, the ThreadPoolExecutor constructor is actually called inside newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
Copy the code
By passing arguments to the constructor, we create a thread pool with the same number of core threads as the maximum number of threads. Their number is the parameter we pass in. The important thing here is that the queue we use is an unlimited LinkedBlockingQueue. More and more tasks will accumulate in the queue, and eventually a large number of tasks will occupy a large amount of memory, and OOM (OutOfMemoryError) will occur, which will almost affect the entire program, resulting in serious consequences.
- SingleThreadExecutor
The second thread pool is SingleThreadExecutor. Let’s look at the source code to create it.
public static ExecutorService newSingleThreadExecutor(a) {
return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1.1.0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
Copy the code
NewSingleThreadExecutor and newFixedThreadPool work the same way, except that the core thread count and the maximum thread count are set directly to 1, but the task queue is still unbounded LinkedBlockingQueue. This causes the same problem, which can take up a lot of memory and result in OOM when tasks pile up.
- CachedThreadPool
The third thread pool is CachedThreadPool, which is created in the source code below.
public static ExecutorService newCachedThreadPool(a) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
Copy the code
The CachedThreadPool differs from the previous two thread pools in that the task queue uses SynchronousQueue. SynchronousQueue does not store tasks, but forwards them directly, which is fine in itself. However, you will notice that the constructor’s second parameter is set to integer. MAX_VALUE, which means the maximum number of threads. Since CachedThreadPool does not limit the number of threads, it is possible to create too many threads when the number of tasks is too large. Eventually, the operating system’s limit is exceeded and new threads cannot be created, or memory runs out.
- ScheduledThreadPool and SingleThreadScheduledExecutor
The fourth thread pool ScheduledThreadPool and 5 kinds of thread pool SingleThreadScheduledExecutor principle is the same, create ScheduledThreadPool source as shown below.
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
Copy the code
Here is ScheduledThreadPoolExecutor ThreadPoolExecutor subclasses, call its constructor is shown below.
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}
Copy the code
The queue is DelayedWorkQueue, which is both a delayed queue and an unbounded queue, so like LinkedBlockingQueue, if there are too many tasks in the queue, it will result in OOM.
You can see, these are automatically created thread pool all the risks, by comparison, we manually create will be better, because we can more clear the thread pool operation rules, not only can choose the number of threads that suits oneself, more can refuse new task submitted, when necessary to avoid the risk of resource depletion.
Thread count setting
The primary purpose of adjusting the number of threads in the thread pool is to maximize program performance by using resources such as CPU and memory efficiently. In practical work, we need to choose corresponding strategies according to different task types.
CPU intensive task
Cpu-intensive tasks, such as encryption, decryption, compression, and computing, require a lot of CPU resources. The optimal number of threads for such a task is 1-2 times the number of CPU cores, and setting too many threads is not going to be very effective. Suppose that the number of threads we set is more than twice the number of CPU cores. Because the computing task is very heavy, it will occupy a lot of CPU resources. Therefore, each core work of CPU is basically full load, and we set too many threads, each thread wants to use CPU resources to execute its own task. This leads to unnecessary context switches, where the number of threads does not improve performance, but rather degrades performance because there are too many threads.
In this case, it is also a good idea to consider what other programs are running on the same machine that may use too much CPU, and then balance the resource usage as a whole.
Time-consuming I/O tasks
The second type of task is time-consuming I/O, such as database, file reading and writing, network communication, etc. This type of task does not consume CPU resources, but I/O operations are time-consuming, and generally occupy a lot of time. For this type of task, the maximum number of threads is usually many times larger than the CPU core, because the IO read and write speed is relatively slow compared to the CPU speed. If we set the number of threads too small, it may lead to a waste of CPU resources. And more if we set the number of threads, so when the part of the thread is waiting for IO, they don’t need to calculate the CPU at this time, then other threads can use the CPU to perform other tasks, each other, so the task queue waiting for the task would be reduced, you can better use of resources.
Brain Goetz, author of Java Concurrent Programming In Action, recommends the following calculation method:
Number of threads = number of CPU cores * (1+ Average waiting time/Average working time)Copy the code
Using this formula, we can calculate a reasonable number of threads that increase if the average wait time of a task is long, and decrease if the average work time is long, i.e. for our cpu-intensive tasks above.
Too few threads can degrade overall program performance, and too many threads can consume memory and other resources. Therefore, if you want to be more accurate, you can perform a pressure test to monitor JVM threads and CPU load, and measure the number of threads that should be created based on the actual situation to make the best use of resources.
conclusion
From what has been discussed above, we may come to the conclusion that
-
The higher the percentage of average working time of threads, the fewer threads are needed;
-
The higher the average waiting time ratio of threads, the more threads are needed;
-
For different programs, the corresponding actual test can get the most appropriate choice.
Custom thread pools
- Core threads
The first parameter to set is corePoolSize, which is the number of core threads that need to be set. The number of threads that need to be set depends on the type of task and the number of CPU cores. The higher the proportion of the average wait time for a thread, the more threads are required. For the maximum number of threads, if the type of task we perform is not fixed, for example, it may be CPU intensive at one time, IO intensive at another time, or there may be a mix of two tasks at the same time. In this case, we can set the maximum number of threads to several times the number of core threads in case of unexpected tasks. Of course, it is better to use different thread pools to perform different types of tasks, so that the tasks are separated by type rather than jumbled together, so that a reasonable number of threads can be set based on the number of threads estimated in the previous class or the results of the pressure test, and achieve better performance.
- Blocking queue
For blocking queue, we can choose the LinkedBlockingQueue or SynchronousQueue or DelayedWorkQueue described earlier, but there is also a common blocking queue called ArrayBlockingQueue. It is also commonly used in thread pools. This blocking queue is implemented internally as an array, requires a capacity value to be passed in when creating an object, and cannot be expanded later, so ArrayBlockingQueue has a limited capacity. This way, if the task queue is full and the number of threads has reached its maximum, the thread pool will reject new submitted tasks according to the rule, which may result in some data loss.
However, it is better to lose data than to run out of memory and crash by increasing the number of tasks or threads indefinitely. If we use ArrayBlockingQueue and limit the number of threads, we can prevent resource depletion very effectively. If we use a larger queue with a smaller maximum number of threads, we can reduce the overhead of context switching, but also reduce the overall throughput. If our task is IO intensive, we can choose a smaller queue with a larger maximum number of threads, which is more efficient overall, but also introduces more context switches.
- Thread factory
For the threadFactory threadFactory parameter, we can either use the default defaultThreadFactory, or we can pass in a custom threadFactory with additional capabilities, because we may have multiple thread pools, and it is necessary to distinguish the different thread pools by different names. So you can pass in thread factories that can be named according to the business information, so that you can later distinguish between different businesses based on the thread name and quickly locate the problem code. Such as through com.google.com mon. Util. Concurrent. ThreadFactory
Builder, as shown in the code.
ThreadFactoryBuilder builder = new ThreadFactoryBuilder();
ThreadFactory rpcFactory = builder.setNameFormat("rpc-pool-%d").build();
Copy the code
ThreadFactory (rpcFactory); rpcFactory (rpcFactory); rpcFactory (rpcFactory); rpcFactory (rpcFactory); “Rpc-pool-2”, and so on.
- Rejection policies
The last parameter is the reject policy. We can use one of the four reject policies described in Lecture 11: AbortPolicy, DiscardPolicy, DiscardOldestPolicy, or CallerRunsPolicy based on business requirements. The RejectedExecutionHandler interface can also be used to implement the rejectedExecution method. Perform user-defined denial policies, such as printing logs, temporary tasks, and re-execution, to meet service requirements. This is shown in the code.
private static class CustomRejectionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// Reject policies such as printing logs, temporary tasks, and re-execution}}Copy the code
conclusion
So customize your own thread pool and is strongly related to our business, first we need to know the meaning of each parameter, as well as the common options, and then according to the actual needs, such as concurrency, memory size, a series of factors such as whether to accept the task was refused to customize a thread pool, very suitable for their own business such both neither can lead to insufficient memory, At the same time, an appropriate number of threads can be used to ensure the efficiency of task execution, and the rejection of tasks can be recorded to facilitate traceability in the future
Close the thread pool properly
Create a thread pool with a fixed number of threads of 10 and submit 100 tasks to the pool, as shown in the code.
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
service.execute(new Task());
}
Copy the code
So what if we now want to close the thread pool? This lesson focuses on the five methods involved in closing thread pools in ThreadPoolExecutor, as shown below.
void shutdown;
boolean isShutdown;
boolean isTerminated;
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
List<Runnable> shutdownNow;
Copy the code
- shutdown()
The first method, called shutdown(), can safely shutdown a thread pool. The pool is not shutdown immediately after the shutdown() method is called because there may be many tasks in the pool that are being executed, or there may be a large number of tasks waiting to be executed in the task queue. After the shutdown() method is called, the thread pool is completely shutdown after executing the tasks in progress and the tasks waiting in the queue. This does not mean that the shutdown() operation has no effect. If a new task is submitted after the shutdown() method is called, the thread pool will reject subsequent new tasks according to the rejection policy.
- isShutdown()
The second method, isShutdown(), returns true or false to determine whether the shutdown or shutdownNow methods have been executed. Note that if the isShutdown() method returns true, it does not mean that the thread pool is completely closed. It simply means that the thread pool has started the process of closing. In other words, there may still be threads executing tasks in the thread pool, and there may still be tasks waiting to be executed in the queue
- isTerminated()
The third method is called isTerminated(), which checks to see if the thread pool is truly “terminated.” This means not only that the pool is closed, but that all tasks in the pool have been completed, because as we said, after calling shutdown, The thread pool continues to execute unfinished tasks, not only tasks that are being executed by the thread, but also tasks that are waiting in the task queue. For example, if the shutdown method is called and one thread is still executing, isShutdown returns true, while isTerminated returns false because there are other tasks being executed in the thread pool. Thread pools are not really “finalised”. The isTerminated() method does not return true until all tasks have been executed, indicating that the thread pool is closed and empty inside and that all remaining tasks have been executed.
- awaitTermination()
The fourth method, called awaitTermination(), is not used to close the thread pool itself, but primarily to determine the state of the thread pool. For example, if we pass 10 seconds to the awaitTermination method, it will wait for 10 seconds until one of three things happens:
1. The thread pool has been closed and all submitted tasks (both executing and queuing) have been completed. The thread pool has been “terminated” and the method returns true.
2. The first thread pool “terminates” condition does not occur after the timeout, and the method returns false.
3. If the thread is interrupted while waiting, the method throws InterruptedException.
That is, after the awaitTermination method is called, the current thread attempts to wait for a specified amount of time. The method returns true if the thread pool is closed and all internal tasks have been executed during the wait time, that is, the pool is truly “terminated.” Otherwise, fasLE is returned after timeout.
We can determine what to do next based on the Boolean value returned by awaitTermination().
- shutdownNow()
The final method, shutdownNow(), is the most powerful of the five. It differs from the first method by adding the word Now to its name, which means shutdown immediately. After the shutdownNow method is executed, all threads in the thread pool are first sent an interrupt signal to try to interrupt the execution of these tasks, and then all tasks waiting in the task queue are moved to a List and returned, We can do some remedial actions based on the returned task List, such as logging and retry later. The source code for shutdownNow() is shown below.
public List<Runnable> shutdownNow(a) {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
Copy the code
The source code contains one line of interruptWorkers() code, which interrupts each thread that has been started so that the thread can detect an interrupt signal during the execution of a task and process it accordingly, prematurely terminating the task. It is important to note that due to the fact that there is no recommended mechanism to forcibly stop threads in Java, even if shutdownNow is invoked, it is still possible that the task will not stop if the interrupted thread ignores the interrupt signal. So it’s important that we implement best practices in our development. Our own threads should be able to respond to interrupt signals. The proper way to stop threads was discussed in lecture 2, and interrupt signals should be used to work together.
With these five methods for shutting down the thread pool, you can choose the appropriate method to stop the thread pool based on your business needs. For example, you can usually shutdown the thread pool with shutdown(), which will allow all committed tasks to complete, but in case of an emergency, We can then use the shutdownNow method to speed up the thread pool “terminating”.
Principle of thread reuse
Thread pool will use fixed number or variable number of threads to perform the task, but whether fixed or variable number of threads, the number of threads are far less than the number of tasks, and faced with this situation the thread pool can thread to reuse the same thread to perform different tasks, so what is the thread to reuse the principle behind?
Thread pooling can decouple threads from tasks, threading by Thread and task by task, getting rid of the previous restriction that one Thread must correspond to one task when creating threads through threads. In the Thread pool, the same Thread can constantly pull new tasks from BlockingQueue to execute. The core principle is that Thread pools encapsulate threads. Thread.start() is not called every time a task is executed. Instead, each thread is asked to execute a “loop task”, in which it constantly checks to see if there are any more tasks waiting to be executed, and if there are, executes the task directly, calling the run method of the task as if it were a normal method. The run() methods for each task are concatenated, so the number of threads does not increase.