为什么阿里巴巴要禁用Executors创建线程池?
看阿里巴巴开发手册并发编程这块有一条:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,通过源码分析禁用的原因
写在前面
首先感谢大家在盖楼的间隙阅读本篇文章,通过阅读本篇文章你将了解到:
- 线程池的定义
- Executors创建线程池的几种方式
- ThreadPoolExecutor对象
- 线程池执行任务逻辑和线程池参数的关系
- Executors创建返回ThreadPoolExecutor对象
- OOM异常测试
- 如何定义线程池参数
线程池的定义
管理一组工作线程。通过线程池复用线程有以下几点优点:
- 减少资源创建 => 减少内存开销,创建线程占用内存
- 降低系统开销 => 创建线程需要时间,会延迟处理的请求
- 提高稳定稳定性 => 避免无限创建线程引起的
OutOfMemoryError
【简称OOM】
Executors创建线程池的方式
根据返回的对象类型创建线程池可以分为三类:
- 创建返回ThreadPoolExecutor对象
- 创建返回ScheduleThreadPoolExecutor对象
- 创建返回ForkJoinPool对象
本文只讨论创建返回ThreadPoolExecutor
对象
ThreadPoolExecutor对象
在介绍Executors
创建线程池方法前先介绍一下ThreadPoolExecutor
,因为这些创建线程池的静态方法都是返回ThreadPoolExecutor
对象,和我们手动创建ThreadPoolExecutor
对象的区别就是我们不需要自己传构造函数的参数。ThreadPoolExecutor
的构造函数共有四个,但最终调用的都是同一个:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
复制代码
构造函数参数说明:
- corePoolSize => 线程池核心线程数量
- maximumPoolSize => 线程池最大数量
- keepAliveTime => 空闲线程存活时间
- unit => 时间单位
- workQueue => 线程池所使用的缓冲队列
- threadFactory => 线程池创建线程使用的工厂
- handler => 线程池对拒绝任务的处理策略
int corePoolSize:该线程池中核心线程数最大值
**核心线程:**线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。
如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉。
int maximumPoolSize: 该线程池中线程总数最大值
线程总数 = 核心线程数 + 非核心线程数。
long keepAliveTime:该线程池中非核心线程闲置超时时长
一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉,如果设置allowCoreThreadTimeOut = true,则会作用于核心线程。
**TimeUnit unit:**keepAliveTime的单位
TimeUnit是一个枚举类型,其包括:
NANOSECONDS : 1微毫秒 = 1微秒 / 1000
MICROSECONDS : 1微秒 = 1毫秒 / 1000
MILLISECONDS : 1毫秒 = 1秒 /1000
SECONDS : 秒
MINUTES : 分
HOURS : 小时
DAYS : 天
BlockingQueue workQueue:
该线程池中的任务队列:维护着等待执行的Runnable对象
当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。
常用的workQueue类型:
- SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大
- LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize
- ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误!
- DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务
ThreadFactory
创建线程的方式,可以用于指定线程名称
RejectedExecution Handler handler:
这玩意儿就是抛出异常专用的,比如上面提到的两个错误发生了,就会由这个handler抛出异常.
ThreadPoolExecutor
创建线程池的方式:
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(16,16,10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100),new DaemonThreadFactory("线程名称"));
//允许核心线程超时
poolExecutor.allowCoreThreadTimeOut(true);
return poolExecutor;
线程池执行任务逻辑和线程池参数的关系
执行逻辑说明:
- 判断核心线程数是否已满,核心线程数大小和
corePoolSize
参数有关,未满则创建线程执行任务 - 若核心线程池已满,判断队列是否满,队列是否满和
workQueue
参数有关,若未满则加入队列中 - 若队列已满,判断线程池是否已满,线程池是否已满和
maximumPoolSize
参数有关,若未满创建线程执行任务 - 若线程池已满,则采用拒绝策略处理无法执执行的任务,拒绝策略和
handler
参数有关
Executors创建方式(不推荐):
Executors创建返回ThreadPoolExecutor对象
Executors
创建返回ThreadPoolExecutor对象的方法共有三种:
- Executors#newCachedThreadPool => 创建可缓存的线程池
- Executors#newSingleThreadExecutor => 创建单线程的线程池
- Executors#newFixedThreadPool => 创建固定长度的线程池
Executors#newCachedThreadPool方法
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
复制代码
CachedThreadPool
是一个根据需要创建新线程的线程池
- corePoolSize => 0,核心线程池的数量为0
- maximumPoolSize => Integer.MAX_VALUE,可以认为最大线程数是无限的
- keepAliveTime => 60L
- unit => 秒
- workQueue => SynchronousQueue
当一个任务提交时,corePoolSize
为0不创建核心线程,SynchronousQueue
是一个不存储元素的队列,可以理解为队里永远是满的,因此最终会创建非核心线程来执行任务。对于非核心线程空闲60s时将被回收。因为Integer.MAX_VALUE非常大,可以认为是可以无限创建线程的,在资源有限的情况下容易引起OOM异常
Executors#newSingleThreadExecutor方法
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
复制代码
SingleThreadExecutor
是单线程线程池,只有一个核心线程
- corePoolSize => 1,核心线程池的数量为1
- maximumPoolSize => 1,只可以创建一个非核心线程
- keepAliveTime => 0L
- unit => 秒
- workQueue => LinkedBlockingQueue
当一个任务提交时,首先会创建一个核心线程来执行任务,如果超过核心线程的数量,将会放入队列中,因为LinkedBlockingQueue是长度为Integer.MAX_VALUE的队列,可以认为是无界队列,因此往队列中可以插入无限多的任务,在资源有限的时候容易引起OOM异常,同时因为无界队列,maximumPoolSize
和keepAliveTime
参数将无效,压根就不会创建非核心线程
Executors#newFixedThreadPool方法
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
复制代码
FixedThreadPool
是固定核心线程的线程池,固定核心线程数由用户传入
- corePoolSize => 1,核心线程池的数量为1
- maximumPoolSize => 1,只可以创建一个非核心线程
- keepAliveTime => 0L
- unit => 秒
- workQueue => LinkedBlockingQueue 它和
SingleThreadExecutor
类似,唯一的区别就是核心线程数不同,并且由于使用的是LinkedBlockingQueue,在资源有限的时候容易引起OOM异常
总结:
- FixedThreadPool和SingleThreadExecutor => 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而引起
OOM
异常 - CachedThreadPool => 允许创建的线程数为Integer.MAX_VALUE,可能会创建大量的线程,从而引起
OOM
异常
这就是为什么禁止使用Executors
去创建线程池,而是推荐自己去创建ThreadPoolExecutor
的原因
ThreadExecutor => 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而引起OOM
异常
- CachedThreadPool => 允许创建的线程数为Integer.MAX_VALUE,可能会创建大量的线程,从而引起
OOM
异常
这就是为什么禁止使用Executors
去创建线程池,而是推荐自己去创建ThreadPoolExecutor
的原因