Thinking logic of computer programs (78) and thinking 78
In the previous section, we initially discussed the task execution service in Java and package. In reality, the main implementation mechanism of the task execution service is the thread pool. In this section, we will discuss the thread pool.
Basic Concepts
A thread pool, as its name implies, is a thread pool. There are several threads in it. They are used to execute the tasks submitted to the thread pool and will not exit after a task is executed, instead, wait for or execute a new task. The thread pool consists of two concepts: one is the task queue and the other is the worker thread. The worker thread subject is a loop that receives and executes tasks from the queue, the task queue stores the tasks to be executed.
The concept of thread pool is similar to some queuing scenarios in life, such as queuing for tickets at the railway station, queuing for registration at the hospital, and queuing for services at the bank. It is generally provided by several windows, these service windows are similar to the worker threads, while the queue concept is similar. However, in actual scenarios, each window often has a separate queue, which is hard to be fair, with the development of information technology, more and more queuing scenarios use virtual unified queues. Generally, a queuing number is used first, and then services are performed by number.
The advantages of the thread pool are obvious:
- It can reuse threads to avoid thread creation overhead.
- When there are too many tasks, avoid creating multiple threads by queuing, reduce system resource consumption and competition, and ensure tasks are completed in an orderly manner.
The implementation class of the thread pool in the Java concurrent package is ThreadPoolExecutor, which inherits from AbstractExecutorService and implements ExecutorService. Its basic usage is similar to that described in the previous section. However, ThreadPoolExecutor has some important parameters. Understanding these parameters is very important for rational use of the thread pool. We will discuss these parameters.
Understanding Thread Pool
Constructor
ThreadPoolExecutor has multiple constructor methods and requires some parameters. The main constructor methods include:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
The second constructor has two more parameters: threadFactory and handler. These two parameters are generally not required. The first constructor sets the default value.
CorePoolSize, maximumPoolSize, keepAliveTime, and unit are used to control the number of threads in the thread pool. workQueue indicates the task queue. threadFactory is used to configure the created thread, and handler indicates the task rejection policy. Next we will discuss these parameters in detail.
Thread pool size
The thread pool size is mainly related to four parameters:
- CorePoolSize: number of core threads
- MaximumPoolSize: Maximum number of threads
- KeepAliveTime and unit: idle thread survival time
MaximumPoolSize indicates the maximum number of threads in the thread pool, and the number of threads changes dynamically. However, this is the maximum value. No matter how many tasks there are, no threads larger than this value will be created.
CorePoolSize indicates the number of core threads in the thread pool. However, this does not mean that such multithreading is created at the beginning. After a thread pool is created, no threads are actually created.
Generally, when a new task arrives, if the number of threads is smaller than corePoolSiz, a new thread is created to execute the task, A new thread is created even if other threads are idle.
However, if the number of threads is greater than or equal to corePoolSiz, a new thread will not be created immediately. It will first try to queue. It must be emphasized that it is a "try" queue, instead of "blocking wait", if the queue is full or cannot join immediately for other reasons, it will not queue, but check whether the number of threads has reached maximumPoolSize. If not, the thread will be created until the number of threads reaches maximumPoolSize.
The purpose of keepAliveTime is to release redundant thread resources. It indicates that when the number of threads in the thread pool is greater than corePoolSize, the survival time of additional Idle threads, that is, a non-core thread, when you wait for a new task in the idle state, there will be a maximum waiting time, namely, keepAliveTime. If there is still no new task at the time, it will be terminated. If the value is 0, it indicates that all threads will not time out and terminate.
In addition to being specified in the constructor, you can also view and modify these parameters using the getter/setter method.
public void setCorePoolSize(int corePoolSize)public int getCorePoolSize()public int getMaximumPoolSize()public void setMaximumPoolSize(int maximumPoolSize)public long getKeepAliveTime(TimeUnit unit)public void setKeepAliveTime(long time, TimeUnit unit)
In addition to these static parameters, ThreadPoolExecutor can also view some dynamic numbers about threads and the number of tasks:
// Return the current number of threads public int getPoolSize () // return the maximum number of threads that the thread pool has reached public int getLargestPoolSize () // return the number of all completed tasks in the thread pool since creation. public long getCompletedTaskCount () // return the number of all tasks, including all completed and all public long getTaskCount () waiting for execution in the queue ()
Queue
ThreadPoolExecutor requires that the queue type be block queue BlockingQueue. We have introduced several types of BlockingQueue in section 76, which can be used as the queue of the thread pool, for example:
- LinkedBlockingQueue: a blocking Queue Based on a linked list. The maximum length can be specified, but it is unbounded by default.
- ArrayBlockingQueue: array-based bounded blocking queue
- PriorityBlockingQueue: heap-based unbounded blocking priority queue
- SynchronousQueue: synchronization blocking queue with no actual storage space
If an unbounded queue is used, it should be emphasized that the maximum number of threads can only reach corePoolSize. After corePoolSize is reached, new tasks will always be queued, and the parameter maximumPoolSize is meaningless.
On the other hand, for SynchronousQueue, we know that it does not have space for actually storing elements. When queuing is attempted, only Idle threads waiting to receive tasks will be successfully queued. Otherwise, A new thread is always created until maximumPoolSize is reached.
Task rejection policy
If the queue is bounded and maximumPoolSize is limited, when the queue is full and the number of threads reaches maximumPoolSize, how can we deal with the new task? In this case, the thread pool's task denial policy is triggered.
By default, methods for submitting tasks, such as execute, submit, and invokeAll, throw an exception of the RejectedExecutionException type.
However, the denial policy can be customized. ThreadPoolExecutor implements four processing methods:
- ThreadPoolExecutor. AbortPolicy: This is the default method and throws an exception.
- ThreadPoolExecutor. DiscardPolicy: silent processing, ignore new tasks, do not throw exceptions or execute
- ThreadPoolExecutor. DiscardOldestPolicy: throws the task with the longest wait time and queues it by yourself.
- ThreadPoolExecutor. CallerRunsPolicy: Execute the task in the task submitter thread rather than in the thread pool.
They are all public static internal classes of ThreadPoolExecutor and all implement the RejectedExecutionHandler interface, which is defined:
public interface RejectedExecutionHandler { void rejectedExecution(Runnable r, ThreadPoolExecutor executor);}
When the thread pool cannot accept tasks, it calls the rejectedExecution method of its rejection policy.
The denial policy can be specified in the constructor or as follows:
public void setRejectedExecutionHandler(RejectedExecutionHandler handler)
The default RejectedExecutionHandler is an AbortPolicy instance, as shown below:
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
The rejectedExecution Implementation of AbortPolicy throws an exception as follows:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString());}
We need to emphasize that a denial policy is triggered only when the queue is bounded and maximumPoolSize is limited.
If the queue is unbounded, tasks that cannot be served will always be queued, but this is not expected, because the request processing queue may consume a very large amount of memory or even cause exceptions with insufficient memory.
If the queue is bounded but maximumPoolSize is infinite, too many threads may be created, occupying the CPU and memory, making it difficult to complete any task.
Therefore, in scenarios with a large number of tasks, it is important to ensure the stable operation of the system to give the denial policy a chance to execute it.
Thread factory
The thread pool can also accept a parameter ThreadFactory, which is an interface defined:
public interface ThreadFactory { Thread newThread(Runnable r);}
This interface creates a Thread Based on Runnable. The default Implementation of ThreadPoolExecutor is the static internal class defathreadfactory In the Executors class. It mainly creates a Thread, sets a Thread name, and sets the daemon attribute to false, set the thread priority to the standard default priority. The thread name format is pool-<thread pool number>-thread-<thread number>.
If you need to customize some thread attributes, such as names, you can implement custom ThreadFactory.
Special configuration of core threads
When the number of threads is less than or equal to corePoolSize, we call these threads as the core threads. By default:
- The Core Thread is not created in advance, and is created only when a task exists.
- The core thread will not be terminated because it is idle. The keepAliveTime parameter is not applicable to it.
However, ThreadPoolExecutor can change the default behavior as follows.
// Pre-create all core threads public int prestartAllCoreThreads () // create a core thread. If all core threads have been created, return falsepublic boolean prestartCoreThread () // If the parameter is true, The keepAliveTime parameter is also applicable to the public void allowCoreThreadTimeOut (boolean value) of the Core Thread)
Factory class Executors
Class Executors provides some static factory methods to easily create pre-configured thread pools. The main methods are as follows:
public static ExecutorService newSingleThreadExecutor()public static ExecutorService newFixedThreadPool(int nThreads)public static ExecutorService newCachedThreadPool()
NewSingleThreadExecutor is basically equivalent to calling:
public static ExecutorService newSingleThreadExecutor() { return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());}
Use only one thread and use unbounded queue blockingqueue. After a thread is created, it does not time out and ends. This thread executes all tasks in sequence. This thread pool is applicable when you need to ensure that all tasks are executed sequentially.
The newFixedThreadPool code is:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());}
Using a fixed number of n threads and using unbounded queue blockingqueue, the thread will not terminate after creation. Like newSingleThreadExecutor, as it is an unbounded queue, if too many tasks are queued, it may consume a very large amount of memory.
The newCachedThreadPool code is:
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());}
Its corePoolSize is 0, maximumPoolSize is Integer. MAX_VALUE, keepAliveTime is 60 seconds, and the queue is SynchronousQueue.
It means that when a new task arrives, if there are just Idle threads waiting for the task, one of the Idle threads will accept the task; otherwise, a new thread will always be created, the total number of created threads is unlimited. For any idle thread, if no new task exists within 60 seconds, it is terminated.
In practice, should we use newFixedThreadPool or newCachedThreadPool?
When the system load is very high, newFixedThreadPool can queue new tasks through the queue to ensure that there are sufficient resources to process the actual tasks, while newCachedThreadPool will create a thread for each task, as a result, too many threads are created to compete for CPU and memory resources, making it difficult to complete any actual task. At this time, newFixedThreadPool is more suitable.
However, if the system load is not high and the execution time of a single task is relatively short, newCachedThreadPool may be more efficient because the task can be directly handed over to a idle thread without waiting in line.
Either of the two is not a good choice when the system load may be extremely high. The problem with newFixedThreadPool is that the queue is too long, and the problem with newCachedThreadPool is that there are too many threads. In this case, you should customize ThreadPoolExecutor according to the actual conditions, pass the appropriate parameters.
Deadlock in the thread pool
For tasks submitted to the thread pool, we need to pay special attention to the dependency between tasks, which may cause deadlocks. For example, task A submits Task B to the same task execution service during its execution, but waits until Task B ends.
If task A is submitted to A single-thread pool, A deadlock will occur. A is waiting for the result of B, and B is waiting for scheduling in the queue.
If it is submitted to a thread pool with a limited number of threads, there may also be deadlocks. Let's look at a simple example:
public class ThreadPoolDeadLockDemo { private static final int THREAD_NUM = 5; static ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM); static class TaskA implements Runnable { @Override public void run() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } Future<?> future = executor.submit(new TaskB()); try { future.get(); } catch (Exception e) { e.printStackTrace(); } System.out.println("finished task A"); } } static class TaskB implements Runnable { @Override public void run() { System.out.println("finished task B"); } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 5; i++) { executor.execute(new TaskA()); } Thread.sleep(2000); executor.shutdown(); }}
The above Code uses newFixedThreadPool to create a thread pool with five threads. The main program submits five TaskA threads, and TaskA submits one TaskB thread and waits until task kb ends, task kb can only wait in queue because the thread is full. In this way, the program will be deadlocked.
How can this problem be solved?
Replace newFixedThreadPool with newCachedThreadPool, so that the creation thread is no longer limited.
Another solution is to use SynchronousQueue to avoid deadlocks. How can this problem be solved? For a common queue, only tasks are put into the queue. For SynchronousQueue, a successful queue means that existing threads are processed. If the queue fails, you can create more threads until maximumPoolSize, if maximumPoolSize is reached, the denial mechanism will be triggered, and no deadlock will occur in any case. We will replace the code for creating executor:
static ExecutorService executor = new ThreadPoolExecutor( THREAD_NUM, THREAD_NUM, 0, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
Only change the queue type and run the same program. The program will not be deadlocked. However, the TaskA submit call will throw an exception RejectedExecutionException, because the queue fails, and the number of threads reaches the maximum value.
Summary
This section describes the basic concepts of the thread pool, discusses in detail the meanings of its main parameters, understanding these parameters is very important for rational use of the thread pool, for mutually dependent tasks, pay special attention to avoid deadlocks.
ThreadPoolExecutor implements the producer/consumer mode. The worker thread is the consumer, the task submitter is the producer, and the thread pool maintains the task queue by itself. When we encounter a problem similar to producer/consumer, we should give priority to directly using the thread pool instead of re-inventing the wheel, and manage and maintain the consumer thread and task queue by ourselves.
In an asynchronous task program, a common scenario is that the main thread submits multiple asynchronous tasks and then processes the results after the tasks are completed, and processes them one by one in the order of task completion, for this scenario, Java concurrent packet sending provides a convenient method. Using CompletionService, let's discuss it in the next section.
(As in other sections, all the code in this section is in the https://github.com/swiftma/program-logic)
----------------
For more information, see the latest article. Please pay attention to the Public Account "lauma says programming" (scan the QR code below), from entry to advanced, ma and you explore the essence of Java programming and computer technology. Retain All copyrights with original intent.