为什么阿里巴巴要禁用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();
    }
}
  1. 按需创建线程,不要一开始就创建corePoolSize个线程,而是在调用者提交任务的过程中逐渐创建出来,最后创建了corePoolSize个就不再创建了
  2. 提高工具的弹性,当任务突增时,队列会被放满,然后多余的任务有可能会被直接扔掉。当然我们可以把corePoolSize设的很大,但是这样并不优雅,因为大部分情况下是用不到这么多线程的。当任务突增时,我们可以适当增加线程,提高执行速度,当然创建的总线程数还是要限制一下的,我们把能创建的总数定为maximumPoolSize
  3. 及时关闭不需要的线程,当任务突增时,线程数可能增加到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💡

  1. FixedThreadPool和SingleThreadPool 使用的是一样的核心线程数和最大线程数。使用LinkedBlockingQueue,无限大队列。容易造成OOM
  2. CacheedThreadPool是不存储数据的队列,所以需要设置线程数为最大。容易造成线程数量太大的OOM
  3. ScheduledThreadPool是无限长”花瓶”,使用DelayedWorkQueue是无界队列。同时最大线程数也是无界的。

FixThreadPool 和 SingleThreadPool(无限队列 + 固定最大线程数)

其它都一样,当FixThreadPool的参数为1时,就是SingleThreadPool。

  1. 都是使用LinkedBlockingQueue。数量为无限大。容易造成OOM

CacheedThreadPool(不存储队列 + 无限最大线程数)

使用的是不存储数据的队列,所以需要设置最大线程数。 最大线程数量太大,就容易OOM,浪费资源。

DelayedWorkQueue(无限队列 + 无限最大线程数)

DelayedWorkQueue 是无界队列, 基于数组实现, 队列的长度可以扩容到 Integer.MAX_VALUE。
同时ScheduledThreadPool的 mamximumPoolSize 也是接近无限大的。
可以想象得到,ScheduledThreadPool就是史上最强花瓶, 极端情况下长度已经突破天际了!

线程池流转

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们
  2. 当执行 execute() 方法添加一个任务时
    1. 如果线程池中的线程数未达到核心线程数,则创建核心线程处理任务。
    2. 如果线程数大于或者等于核心线程数,则将任务加入任务队列中,线程池中的空闲线程会不断的从任务队列中取出任务进行处理。
    3. 如果任务队列满了,并且线程数没有达到最大线程数,则创建非核心线程去处理任务。
    4. 如果线程数超过了最大线程数,则执行上面提到的几种饱和策略。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程空闲时,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize 值,那么这个线程会被销毁。

拒绝策略

  • AbortPolicy,直接抛错异常。
  • CallerRunsPolicy,主线程运行。会导致主线程无法添加任务。
  • 自定义RejectedExecutionHandler。
  • 不重要的话,也可以丢弃。DiscardPolicy或者DiscardOldestPolicy

QA

任务执行异常时,线程池怎么处理?移除并创建新的。

当任务出现未被捕获到的异常时,会将执行该任务的线程池中的线程从线程池移除并结束掉,然后移除之后创建一个新的线程放回到线程池中。

ThreadFactory的作用?给线程取名

给线程池取名,方便后续跟踪排查问题。

 execute 与 submit 方法区别? submit有返回值。

最佳实践

  1. CPU密集型任务:尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
  2. IO密集型任务:可以使用稍大的线程池,一般为2CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
  3. 混合型任务:可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

参考资料

ThreadPoolExecutor参数图解_丶Veer的博客-CSDN博客