Detailed thread pool ThreadPoolExecutor

Disclaimer: This article is a blogger original article, follow the CC 4.0 BY-SA copyright agreement, reproduced, please attach the original source link and this statement.
This link: https://blog.csdn.net/ThinkWon/article/details/102541900

Why use a thread pool

In actual use, the thread is occupied system resources, if not properly managed thread can easily lead to system problems. Thus, in most concurrent framework will use the thread pool to manage threads, thread pool management thread mainly has the following advantages:

  1. Reduce resource consumption . By multiplexing the threads and reduce the number of existing thread closes to reduce system performance loss as much as possible;
  2. Improve system responsiveness . By reusing threads, eliminating the process of creating a thread, and therefore enhance the overall response speed of the system;
  3. Thread improve manageability . A thread is a scarce resource, if the unlimited creation, not only consumes system resources, but also reduce the stability of the system, therefore, need to use the thread pool to manage threads.

Detailed thread pool

Create a thread pool

Create a thread pool is mainly ThreadPoolExecutor class to complete, there are many ThreadPoolExecutor overloaded constructor, most of the parameters by the constructor to understand which parameters need to be configured to create a thread pool there. ThreadPoolExecutor constructor method:

ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

The following parameters will be described:

  1. corePoolSize: indicates the size of the core thread pool. When submitting a job, if the current number of threads in the thread pool does not reach the core corePoolSize, even if the current core thread pool idle threads will create a new thread to perform the task submitted. If the current core number of threads the thread pool has reached corePoolSize, it is not re-create the thread. If you call prestartCoreThread()or prestartAllCoreThreads()thread pool is created when all the core thread will be created and launched.
  2. maximumPoolSize: indicates that the thread pool can create the maximum number of threads. If blocked when the queue is full, and the current does not exceed the number of thread pool threads maximumPoolSize, it would create a new thread to perform the task.
  3. keepAliveTime: idle thread survival time. If the current number of threads in the thread pool has exceeded corePoolSize, and the thread is idle for more than keepAliveTime, it would destroy these idle threads, which can reduce system resource consumption as much as possible.
  4. unit: unit of time. Specified time unit is keepAliveTime.
  5. workQueue: blocking queue. Blocking queue for saving tasks on blocking queue can look at this article . You can use ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue .
  6. threadFactory: Create a factory class thread. You can set more meaningful by specifying the thread is created for each plant out of the thread name, if concurrency problems, but also easy to find the cause of the problem.
  7. handler: saturation strategy. When the thread pool blocking queue is full and the specified thread has been opened, indicating that the current thread pool is already saturated, then we need to adopt a strategy to deal with this situation. These types of strategies used are:
    1. AbortPolicy: direct refusal task submitted, and throw RejectedExecutionException an exception;
    2. CallerRunsPolicy: only to perform tasks with the thread of the caller is located;
    3. DiscardPolicy: do not deal directly discarded task;
    4. DiscardOldestPolicy: discarded the blocking queue storage longest task execution of the current task

Execution logic thread pool

ThreadPoolExecutor created by the thread pool, the task execution process after submitting what, let's take a look through the source code. Source execute method as follows:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
	//如果线程池的线程个数少于corePoolSize则创建新线程执行当前任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
	//如果线程个数大于corePoolSize或者创建线程失败,则将任务存放在阻塞队列workQueue中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
	//如果当前任务无法放进阻塞队列中,则创建新的线程来执行任务
    else if (!addWorker(command, false))
        reject(command);
}

ThreadPoolExecutor execute method execution logic see comments. Performing a schematic diagram below shows the execute method of ThreadPoolExecutor:

Here Insert Picture Description

execution logic execute method has several situations:

  1. If the thread is currently running less than corePoolSize, it will create a new thread to perform new tasks;
  2. If the number of running threads is equal to or greater than corePoolSize, the task will be submitted to the blocking queue stored in workQueue;
  3. If the current workQueue queue is full, then this will create a new thread to perform the task;
  4. If the number of threads has exceeded maximumPoolSize, it will be used to process the saturation strategy RejectedExecutionHandler.

It should be noted that the design of the thread pool is to use a core thread pool corePoolSize, blocking queue workQueue and the maximum number of thread pool threads maximumPoolSize , this caching strategy to handle the task, in fact, such a design would need to be used in the framework .

Close the thread pool

Close the thread pool, you can shutdownand shutdownNowthese two methods. Their principles are through all the threads in the pool, followed by an interrupt thread. shutdownAnd shutdownNowthere are not the same place:

  1. shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表;
  2. shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程

可以看出shutdown方法会将正在执行的任务继续执行完,而shutdownNow会直接中断正在执行的任务。调用了这两个方法的任意一个,isShutdown方法都会返回true,当所有的线程都关闭成功,才表示线程池成功关闭,这时调用isTerminated方法才会返回true。

线程池的工作原理

当一个并发任务提交给线程池,线程池分配线程去执行任务的过程如下图所示:

Here Insert Picture Description

从图可以看出,线程池执行所提交的任务过程主要有这样几个阶段:

  1. 先判断线程池中核心线程池所有的线程是否都在执行任务。如果不是,则新创建一个线程执行刚提交的任务,否则,核心线程池中所有的线程都在执行任务,则进入第2步;
  2. 判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第3步;
  3. 判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则,则交给饱和策略进行处理

线程池阻塞队列

作用:用来存储等待执行的任务

线程的公平访问队列:指阻塞的线程可以按照阻塞的先后顺序访问队列,即先阻塞先访问线程。为了保证公平性,通常会降低吞吐量

常见阻塞队列

ArrayBlockingQueue:一个用数组实现的有界阻塞队列,按照先入先出(FIFO)的原则对元素进行排序。
不保证线程公平访问队列,使用较少

PriorityBlockingQueue:支持优先级的无界阻塞队列,使用较少

LinkedBlockingQueue:一个用链表实现的有界阻塞队列,队列默认和最长长度为Integer.MAX_VALUE。
队列按照先入先出的原则对元素进行排序,使用较多

  • 吞吐量通常要高于 ArrayBlockingQueue
  • Executors.newFixedThreadPool() 使用了这个队列

SynchronousQueue:不储存元素(无容量)的阻塞队列,每个put操作必须等待一个take操作,
否则不能继续添加元素。支持公平访问队列,常用于生产者,消费者模型,吞吐量较高,使用较多

  • 每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态
  • 吞吐量通常要高于 LinkedBlockingQueue
  • Executors.newCachedThreadPool使用了这个队列

线程池的饱和策略

定义:当队列和线程池都满了,说明线程池处于饱和状态,必须采取一种策略处理新提交的任务。

常见策略

AbortPolicy:中断策略,直接抛出异常

CallerRunsPolicy:调用者运行策略,让调用者所在线程来运行策略

DiscardOldestPolicy:舍弃最旧任务策略,丢弃队列中最旧的任务,然后重试任务的提交执行( execute() )

DiscardPolicy:舍弃策略,不处理,直接丢弃

自定义策略

如何合理配置线程池参数?

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

  1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  2. 任务的优先级:高,中和低。
  3. 任务的执行时间:长,中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置CPU个数+1的线程数的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如配置两倍CPU个数+1。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。

当然具体合理线程池值大小,需要结合系统实际情况,在大量的尝试下比较才能得出,以上只是前人总结的规律。

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:

The optimum number of threads = (thread waiting thread CPU time ratio + 1) * Number of CPU

Can draw a conclusion :
thread wait the higher the percentage, the more the need to thread. The higher the percentage of CPU time the thread, need fewer threads.
The number of CPU and IO-intensive tasks set before the thread above formula basically.

References "Art Java concurrent programming."

Guess you like

Origin blog.csdn.net/ThinkWon/article/details/102541900