前一篇文章写道了Executors类,其中提供了几个构造Executor的工厂方法。但在实现上,这些执行器最终都是采用了java.util.concurrent.ThreadPoolExecutor类的对象。接下来的文章我们就来了解和分析一下ThreadPoolExecutor这个类。由于篇幅比较长,这一篇主要从应用的角度,对ThreadPoolExecutor的使用做简单整理,对于任务提交和生命周期管理等源码实现的分析会在后面文章逐步整理。
0. ThreadPoolExecutor的初步认识
在之前整理Java并发开发的文章中提到,在JavaSE5之后,直接使用Thread类并不是被提倡的并发开发方式,而Executor成为主要角色。
这其中有如下几个原因:
- 复用效率。Thread每一次构建和销毁都有一定成本,而线程池执行器中,线程是可以复用的,这在一定程度上降低了线程的维护成本,提高了开发和运行效率。
- 资源限制和管理。前面整理并发的文章中我们也提到了,启用多线程并发开发实际上是为了更充分的利用处理器资源,但这并不意味着开启的线程越多越好。每个线程都占有一定的系统资源,如果为了处理某项任务,对开启线程不加限制,那么最终会使系统资源耗尽导致异常问题发生。因此,对于线程的启用需要有一个限制和管理,而线程池执行器将这些方面融入,降低了并发多线程的管理成本。
- 数据统计维护。除了上述两点以外,对于多线程执行中的一些数据,线程池执行器也进行了维护,方便了并发开发。
Executors这个工具类中提供了如下静态方法:
- Executors.newCachedThreadPool() 无限大小的线程池,线程会自动重用
- Executors.newFixedThreadPool(int) 固定线程数的线程池
- Executors.newSingleThreadExecutor() 单线程执行器
而实际上他们是调用了ThreadPoolExecutor类的构造方法来创建Executor对象的。java.util.concurrent.ThreadPoolExecutor全参数构造方法如下:
1
2
3
4
5
6
7
|
public
ThreadPoolExecutor(
int
corePoolSize,
int
maximumPoolSize,
long
keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
|
ThreadPoolExecutor类一共有4个重载的构造方法,但最终调用的都是这个。这个构造方法的参数翠泉,一共提供了7个参数,这在Java API中的方法中,已经算是比较多的了。7个参数的简要说明如下:
- corePoolSize 是线程池的核心线程数,通常线程池会维持这个线程数
- maximumPoolSize 是线程池所能维持的最大线程数
- keepAliveTime 和 unit 则分别是超额线程的空闲存活时间数和时间单位
- workQueue 是提交任务到线程池的入队列
- threadFactory 是线程池创建新线程的线程构造器
- handler 是当线程池不能接受提交任务的时候的处理策略
更详细的解释下面会分点逐一说明。
1. 线程创建和任务提交的条件和步骤
当一个新任务被提交给ThreadPoolExecutor的时候,处理流程大概是这样的:
- 如果当前线程池中线程的数目低于corePoolSize,则创建新线程执行提交的任务,而无需检查当前是否有空闲线程
- 如果提交任务时线程池中线程数已达到corePoolSize,则将提交的任务加入等待执行队列
- 如果提交任务时等待执行的任务队列是有限队列,而且已满,则在线程池中开辟新线程执行此任务
- 如果线程池中线程数目已达到maximumPoolSize,则提交的任务交由RejectedExecutionHandler处理
默认情况下,ThreadPoolExecutor的线程数是根据需求来延迟初始化的,即有新任务加进来的时候才会挨个创建线程。
除此之外,线程池执行器也提供了提前创建初始化线程的方法:
- public boolean prestartCoreThread()
- public int prestartAllCoreThreads()
分别是预先创建一个线程和预先创建线程直到线程数到达核心线程数corePoolSize。
2. 线程数目的维护
刚刚提到,ThreadPoolExecutor有corePoolSize和maximum两个变量来维护线程池中的线程个数,提交任务的时候会有线程数目的增长,那线程的个数又是怎么来维护的呢。构造方法里还有两个参数,分别是keepAlive和unit,这两个参数确定了一个时间间隔,也就是空闲线程存活的时间间隔。默认情况下,当线程池中的线程个数超出了corePoolSize,那么空闲的线程一旦超出额定的存活时间就会被终止,以节省系统资源。在JDK1.6之后,增加了allowsCoreThreadTimeOut这个boolean属性和读写属性的方法,用来标志核心线程空闲超时是否也可以终止掉。
3. 线程入队列和任务丢弃原则简述
除了前面描述涉及到的四个属性和ThreadFactory之外,还有两个分别是workQueue和handler,分别是BlockingQueue和RejectedExecutionHandler类型。
BlockingQueue只是一个接口,它所表达的是当队列为空或者已满的时候,需要阻塞以等待生产者/消费者协同操作并唤醒线程。其有很多不同的具体实现类,各有特点。有的可以规定队列的长度,也有一些则是无界的。
按照Executors类中的几个工厂方法,分别使用的是:
- LinkedBlockingQueue。FixedThreadPool和SingleThreadExecutor使用的是这个BlockingQueue,队列长度是无界的,适合用于提交任务相互独立无依赖的场景。
- SynchronousQueue。 CachedThreadPool使用的是这个BlockingQueue,通常要求线程池不设定最大的线程数,以保证提交的任务有机会执行而不被丢掉。通常这个适合任务间有依赖的场景。
当然,开发者也可以定制ThreadPoolExecutor时使用ArrayBlockingQueue有界队列。
对于任务丢弃,ThreadPoolExecutor以内部类的形式实现了4个策略。分别是:
- CallerRunsPolicy。提交任务的线程自己负责执行这个任务。
- AbortPolicy。使Executor抛出异常,通过异常做处理。
- DiscardPolicy。丢弃提交的任务。
- DiscardOldestPolicy。丢弃掉队列中最早加入的任务。
在调用构造方法时,参数中未指定RejectedExecutionHandler情况下,默认采用AbortPolicy。