JAVA语言为我们提供了两种基础线程池的选择:ScheduledThreadPoolExecutor
和ThreadPoolExecutor
。它们都实现了ExecutorService
接口(注意,ExecutorService接口本身和“线程池”并没有直接关系,它的定义更接近“执行器”,而“使用线程管理的方式进行实现”只是其中的一种实现方式)。这篇文章中,我们主要围绕 ThreadPoolExecutor 类进行讲解。
ThreadPoolExecutor的构造方法
逻辑结构
1 | public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { |
构造函数中需要传入的参数包括corePoolSize
、maximumPoolSize
、keepAliveTime
、timeUnit
和workQueue
。要明确理解这些参数(和后续将要介绍的参数)的含义,就首先要搞清楚 ThreadPoolExecutor 线程池的逻辑结构。
一定要注意一个概念,即存在于线程池中容器的一定是 Thread 对象,而不是您要求运行的任务(所以叫线程池而不叫任务池也不叫对象池,更不叫游泳池);您要求运行的任务将被线程池分配给某一个空闲的 Thread 运行。从上图中,我们可以看到构成线程池的几个重要元素:
- 等待队列:顾名思义,就是您调用线程池对象的 submit() 方法或者 execute() 方法,要求线程池运行的任务(这些任务必须实现 Runnable 接口或者 Callable 接口)。但是出于某些原因线程池并没有马上运行这些任务,而是送入一个队列等待执行。
- 核心线程:线程池主要用于执行任务的是“核心线程”,“核心线程”的数量是您创建线程时所设置的 corePoolSize 参数决定的。如果不进行特别的设定,线程池中始终会保持 corePoolSize 数量的线程数(不包括创建阶段)。
- 非核心线程:一旦任务数量过多(由等待队列的特性决定),线程池将创建“非核心线程”临时帮助运行任务。您设置的大于 corePoolSize 参数小于 maximumPoolSize 参数的部分,就是线程池可以临时创建的“非核心线程”的最大数量。这种情况下如果某个线程没有运行任何任务,在等待 keepAliveTime 时间后,这个线程将会被销毁,直到线程池的线程数量重新达到corePoolSize。也就是说,并不是所谓的“非核心线程”才会被回收;而是谁的空闲时间达到 keepAliveTime 这个阀值,就会被回收。直到线程池中线程数量等于 corePoolSize 为止。
- maximumPoolSize 参数也是当前线程池允许创建的最大线程数量。那么如果您设置的 corePoolSize 参数和您设置的 maximumPoolSize 参数一致时,线程池在任何情况下都不会回收空闲线程。 keepAliveTime 和 timeUnit 也就失去了意义。
- keepAliveTime 参数和 timeUnit 参数也是配合使用的。 keepAliveTime 参数指明等待时间的量化值, timeUnit 指明量化值单位。例如 keepAliveTime=1,timeUnit 为 TimeUnit.MINUTES ,代表空闲线程的回收阀值为1分钟。
线程池种类
CachedThreadPool:一个任务创建一个线程;
1
2
3
4
5public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
FixedThreadPool:所有任务只能使用固定大小的线程;
1
2
3
4
5
6public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
1
2
3
4
5
6
7public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
完整的任务处理过程
1、首先您可以通过线程池提供的 submit() 方法或者 execute() 方法,要求线程池执行某个任务。线程池收到这个要求执行的任务后,会有几种处理情况:
1.1、如果当前线程池中运行的线程数量还没有达到 corePoolSize 大小时,线程池会创建一个新的线程运行您的任务,无论之前已经创建的线程是否处于空闲状态。
1.2、如果当前线程池中运行的线程数量已经达到设置的 corePoolSize 大小,线程池会把您的这个任务加入到等待队列中。直到某一个的线程空闲了,线程池会根据您设置的等待队列规则,从队列中取出一个新的任务执行。
1.3、如果根据队列规则,这个任务无法加入等待队列。这时线程池就会创建一个“非核心线程”直接运行这个任务。注意,如果这种情况下任务执行成功,那么当前线程池中的线程数量一定大于 corePoolSize 。
1.4、如果这个任务,无法被“核心线程”直接执行,又无法加入等待队列,又无法创建“非核心线程”直接执行,且您没有为线程池设置RejectedExecutionHandler
。这时线程池会抛出 RejectedExecutionException 异常,即线程池拒绝接受这个任务。(实际上抛出 RejectedExecutionException 异常的操作,是 ThreadPoolExecutor 线程池中一个默认的 RejectedExecutionHandler 实现:AbortPolicy
)
2、一旦线程池中某个线程完成了任务的执行,它就会试图到任务等待队列中拿去下一个等待任务(所有的等待任务都实现了BlockingQueue
接口,按照接口字面上的理解,这是一个可阻塞的队列接口),它会调用等待队列的 poll() 方法,并停留在哪里。
3、当线程池中的线程超过您设置的 corePoolSize 参数,说明当前线程池中有所谓的“非核心线程”。那么当某个线程处理完任务后,如果等待 keepAliveTime 时间后仍然没有新的任务分配给它,那么这个线程将会被回收。线程池回收线程时,对所谓的“核心线程”和“非核心线程”是一视同仁的,直到线程池中线程的数量等于您设置的 corePoolSize 参数时,回收过程才会停止,还有回收的前提条件是线程数大于 corePoolSize。
allowCoreThreadTimeOut
前文我们讨论到,线程池回收线程只会发生在当前线程池中线程数量大于 corePoolSize 参数的时候;当线程池中线程数量小于等于 corePoolSize 参数的时候,回收过程就会停止。 allowCoreThreadTimeOut 设置项可以要求线程池:将包括“核心线程”在内的,没有任务分配的任何线程,在等待 keepAliveTime 时间后全部进行回收:
1 | ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(1)); |
prestartAllCoreThreads
前文我们还讨论到,当线程池中的线程还没有达到您设置的 corePoolSize 参数值的时候,如果有新的任务到来,线程池将创建新的线程运行这个任务,无论之前已经创建的线程是否处于空闲状态。这个描述可以用下面的示意图表示出来:
prestartAllCoreThreads
设置项,可以在线程池创建,但还没有接收到任何任务的情况下,先行创建符合 corePoolSize 参数值的线程数:
1 | ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(1)); |
使用ThreadFactory
线程池最主要的一项工作,就是在满足某些条件的情况下创建线程。而在 ThreadPoolExecutor 线程池中,创建线程的工作交给ThreadFactory
来完成。要使用线程池,就必须要指定ThreadFactory。
如果我们使用的构造函数时并没有指定使用的 ThreadFactory ,这个时候 ThreadPoolExecutor 会使用一个默认的 ThreadFactory:DefaultThreadFactory
。
1 | package test.thread.pool; |
线程池的等待队列
在使用 ThreadPoolExecutor 线程池的时候,需要指定一个实现了 BlockingQueue接口的任务等待队列。在 ThreadPoolExecutor 线程池的 API 文档中,一共推荐了三种等待队列,它们是:SynchronousQueue
、LinkedBlockingQueue
和ArrayBlockingQueue
;但通过观察 BlockingQueue 接口的实现情况,您可以发现,能够直接使用的等待队列还有:PriorityBlockingQueue
、LinkedBlockingDeque
和LinkedTransferQueue
。
有限队列
SynchronousQueue
: 一种阻塞队列,其中每个 put 必须等待一个 take,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有。即这是一个内部没有任何容量的阻塞队列,任何一次插入操作的元素都要等待相对的删除/读取操作,否则进行插入操作的线程就要一直等待,反之亦然。1
2
3
4
5SynchronousQueue<Object> queue = new SynchronousQueue<Object>();
// 不要使用add,因为这个队列内部没有任何容量,所以会抛出异常“IllegalStateException”
// queue.add(new Object());
// 操作线程会在这里被阻塞,直到有其他操作线程取走这个对象
queue.put(new Object());
ArrayBlockingQueue
:一个由数组支持的有界阻塞队列。此队列按 FIFO (先进先出)原则对元素进行排序。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。1
2
3
4
5
6
7
8
9// 我们创建了一个ArrayBlockingQueue,并且设置队列空间为2
ArrayBlockingQueue<Object> arrayQueue = new ArrayBlockingQueue<Object>(2);
// 插入第一个对象
arrayQueue.put(new Object());
// 插入第二个对象
arrayQueue.put(new Object());
// 插入第三个对象时,这个操作线程就会被阻塞。
arrayQueue.put(new Object());
// 请不要使用add操作,和SynchronousQueue的add操作一样,它们都使用了AbstractQueue中的add实现
无限队列
LinkedBlockingQueue
: 在 ThreadPoolExecutor 线程池中常应用的等待队列。它可以指定容量也可以不指定容量。由于它具有“无限容量”的特性,所以我还是将它归入了无限队列的范畴(实际上任何无限容量的队列/栈都是有容量的,这个容量就是 Integer.MAX_VALUE )。 LinkedBlockingQueue 的实现是基于链表结构,而不是类似 ArrayBlockingQueue 那样的数组。但实际使用过程中,您不需要关心它的内部实现,如果您指定了 LinkedBlockingQueue 的容量大小,那么它反映出来的使用特性就和 ArrayBlockingQueue 类似了。1
2
3
4
5
6
7
8
9
10
11
12
13
14LinkedBlockingQueue<Object> linkedQueue = new LinkedBlockingQueue<Object>(2);
linkedQueue.put(new Object());
// 插入第二个对象
linkedQueue.put(new Object());
// 插入第三个对象时,这个操作线程就会被阻塞。
linkedQueue.put(new Object());
=======================================
// 或者如下使用:
LinkedBlockingQueue<Object> linkedQueue = new LinkedBlockingQueue<Object>();
linkedQueue.put(new Object());
// 插入第二个对象
linkedQueue.put(new Object());
// 插入第N个对象时,都不会阻塞
linkedQueue.put(new Object());LinkedBlockingDeque
:一个基于链表的双端队列。 LinkedBlockingQueue 的内部结构决定了它只能从队列尾部插入,从队列头部取出元素;但是 LinkedBlockingDeque 既可以从尾部插入/取出元素,还可以从头部插入元素/取出元素。
1 | LinkedBlockingDeque<TempObject> linkedDeque = new LinkedBlockingDeque<TempObject>(); |
PriorityBlockingQueue
: 一个按照优先级进行内部元素排序的无限队列。存放在 PriorityBlockingQueue 中的元素必须实现 Comparable 接口,这样才能通过实现 compareTo() 方法进行排序。优先级最高的元素将始终排在队列的头部; PriorityBlockingQueue 不会保证优先级一样的元素的排序,也不保证当前队列中除了优先级最高的元素以外的元素,随时处于正确排序的位置。LinkedTransferQueue
:一个无限队列,它除了具有一般队列的操作特性外(先进先出),还具有一个阻塞特性: LinkedTransferQueue 可以由一对生产者/消费者线程进行操作,当消费者将一个新的元素插入队列后,消费者线程将会一直等待,直到某一个消费者线程将这个元素取走,反之亦然。
拒绝任务
在 ThreadPoolExecutor 线程池中还有一个重要的接口:RejectedExecutionHandler
。当提交给线程池的某一个新任务无法直接被线程池中“核心线程”直接处理,又无法加入等待队列,也无法创建新的线程执行;又或者线程池已经调用 shutdown() 方法停止了工作;又或者线程池不是处于正常的工作状态;这时候 ThreadPoolExecutor 线程池会拒绝处理这个任务,触发您创建 ThreadPoolExecutor 线程池时定义的 RejectedExecutionHandler 接口的实现。
ThreadPoolExecutor线程池在创建时,会使用一个默认的 RejectedExecutionHandler 接口实现
1 | public class ThreadPoolExecutor extends AbstractExecutorService { |
实际上,在 ThreadPoolExecutor 中已经提供了四种可以直接使用的 RejectedExecutionHandler 接口的实现:
CallerRunsPolicy
:这个拒绝处理器,将直接运行这个任务的 run 方法。但是,请注意并不是在 ThreadPoolExecutor 线程池中的线程中运行,而是直接调用这个任务实现的 run 方法。AbortPolicy
:这个处理器,在任务被拒绝后会创建一个 RejectedExecutionException 异常并抛出。这个处理过程也是 ThreadPoolExecutor 线程池默认的 RejectedExecutionHandler 实现。DiscardPolicy
: DiscardPolicy处理器,将会默默丢弃这个被拒绝的任务,不会抛出异常,也不会通过其他方式执行这个任务的任何一个方法,更不会出现任何的日志提示。DiscardOldestPolicy
:这个处理器很有意思。它会检查当前ThreadPoolExecutor线程池的等待队列。并调用队列的 poll() 方法,将当前处于等待队列列头的等待任务强行取出,然后再试图将当前被拒绝的任务提交到线程池执行。
CallerRunsPolicy 在非线程池以外直接调用任务的 run 方法,可能会造成线程安全上的问题;
DiscardPolicy 默默的忽略掉被拒绝任务,也没有输出日志或者提示,开发人员不会知道线程池的处理过程出现了错误;
DiscardOldestPolicy 中 e.getQueue().poll() 的方式好像是科学的,但是如果等待队列出现了容量问题,大多数情况下就是这个线程池的代码出现了 BUG 。
最科学的的还是 AbortPolicy 提供的处理方式:抛出异常,由开发人员进行处理。