为什么阿里巴巴要禁用Executors创建线程池
技巧💡
线程池,主要是用来线程复用。是一个池化的思想。
线程池演进之路
public class AsyncExecutorV4 implements Executor {
private BlockingQueue<Runnable> workQueue;
private List<WorkThread> workThreadList = new ArrayList<>();
private RejectedExecutionHandler handler;
public AsyncExecutorV4(int corePoolSize,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler,
ThreadFactory threadFactory) {
this.workQueue = workQueue;
this.handler = handler;
for (int i = 0; i < corePoolSize; i++) {
// 用工厂类创建线程
WorkThread workThread = threadFactory.newThread();
workThread.start();
workThreadList.add(workThread);
}
}
@SneakyThrows
@Override
public void execute(Runnable r) {
if (!workQueue.offer(r)) {
handler.rejectedExecution(r);
}
}
// 异步线程
public class WorkThread extends Thread {
@Override
public void run() {
while (true) {
Runnable task = null;
try {
task = workQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
task.run();
}
}
}
// 异步线程工厂类
public interface ThreadFactory {
WorkThread newThread();
}
}
- 按需创建线程,不要一开始就创建corePoolSize个线程,而是在调用者提交任务的过程中逐渐创建出来,最后创建了corePoolSize个就不再创建了
- 提高工具的弹性,当任务突增时,队列会被放满,然后多余的任务有可能会被直接扔掉。当然我们可以把corePoolSize设的很大,但是这样并不优雅,因为大部分情况下是用不到这么多线程的。当任务突增时,我们可以适当增加线程,提高执行速度,当然创建的总线程数还是要限制一下的,我们把能创建的总数定为maximumPoolSize
- 及时关闭不需要的线程,当任务突增时,线程数可能增加到maximumPoolSize,但是大多数时间corePoolSize个线程就足够用了,因此可以定义一个超时时间,当一个线程在keepAliveTime时间内没有执行任务,就把它给关掉。 通过queufe.poll() 超时时间来获取。
创建线程池的方式
- 创建返回ThreadPoolExecutor对象
- 创建返回ScheduleThreadPoolExecutor对象
- 创建返回ForkJoinPool对象
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 => 线程池对拒绝任务的处理策略
可以理解为瓶子。瓶口 、 瓶颈 、 瓶身。
阿里的规范
Excutors来创建,可能会造成OOM💡
- FixedThreadPool和SingleThreadPool 使用的是一样的核心线程数和最大线程数。使用LinkedBlockingQueue,无限大队列。容易造成OOM
- CacheedThreadPool是不存储数据的队列,所以需要设置线程数为最大。容易造成线程数量太大的OOM
- ScheduledThreadPool是无限长”花瓶”,使用DelayedWorkQueue是无界队列。同时最大线程数也是无界的。
FixThreadPool 和 SingleThreadPool(无限队列 + 固定最大线程数)
其它都一样,当FixThreadPool的参数为1时,就是SingleThreadPool。
- 都是使用LinkedBlockingQueue。数量为无限大。容易造成OOM
CacheedThreadPool(不存储队列 + 无限最大线程数)
使用的是不存储数据的队列,所以需要设置最大线程数。 最大线程数量太大,就容易OOM,浪费资源。
DelayedWorkQueue(无限队列 + 无限最大线程数)
DelayedWorkQueue 是无界队列, 基于数组实现, 队列的长度可以扩容到 Integer.MAX_VALUE。
同时ScheduledThreadPool的 mamximumPoolSize 也是接近无限大的。
可以想象得到,ScheduledThreadPool就是史上最强花瓶, 极端情况下长度已经突破天际了!
线程池流转
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们
- 当执行 execute() 方法添加一个任务时
- 如果线程池中的线程数未达到核心线程数,则创建核心线程处理任务。
- 如果线程数大于或者等于核心线程数,则将任务加入任务队列中,线程池中的空闲线程会不断的从任务队列中取出任务进行处理。
- 如果任务队列满了,并且线程数没有达到最大线程数,则创建非核心线程去处理任务。
- 如果线程数超过了最大线程数,则执行上面提到的几种饱和策略。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程空闲时,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize 值,那么这个线程会被销毁。
拒绝策略
- AbortPolicy,直接抛错异常。
- CallerRunsPolicy,主线程运行。会导致主线程无法添加任务。
- 自定义RejectedExecutionHandler。
- 不重要的话,也可以丢弃。DiscardPolicy或者DiscardOldestPolicy
QA
任务执行异常时,线程池怎么处理?移除并创建新的。
当任务出现未被捕获到的异常时,会将执行该任务的线程池中的线程从线程池移除并结束掉,然后移除之后创建一个新的线程放回到线程池中。
ThreadFactory的作用?给线程取名
给线程池取名,方便后续跟踪排查问题。
execute 与 submit 方法区别? submit有返回值。
最佳实践
- CPU密集型任务:尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
- IO密集型任务:可以使用稍大的线程池,一般为2CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
- 混合型任务:可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。