我悟了!难怪阿里禁止使用Executors 创建线程池
阿里的《Java 开发手册》有下面这一条编程规约。
一开始我是百思不得其解,直到我了解了Executors线程池.
要想深刻的理解这一条,我们需要了解线程池的几个参数。我们看下ThreadPoolExecutor构造函数所需要的参数。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler{
//省略实现
}
参数的含义如下
- corePoolSize => 核心线程数量
- maximunPoolSize => 最大线程数量
- keepAliveTime => 允许线程空闲的时间。空闲时间超过该数值会进行相应的处理
- unit => keepAliveTime的基本单位,常用的有毫秒和秒
- workQueue 线程池的工作队列,属于阻塞队列
- threadFactory 线程工厂,用于创建线程
- handler 线程池已满对新来任务采取的拒绝策略,也称饱和策略
当有一个新的任务来到线程池,线程池当前线程数量为poolSize
- 如果poolSize<corePoolSize,则创建新的线程来执行任务
- 如果poolSize>=corePoolSize,但是workQueue工作队列还没有满,则任务进入到工作队列中。
- 如果工作队列已满,但是poolSize<maxmumPoolSize,则创建新的线程来执行任务
- 如果poolSize>=maxmumPoolSize,则对于新来的任务根据指定的饱和策略进行处理
饱和策略主要有以下几种
- AbortPolicy => 默认策略,直接抛出RejectedExecutionException异常
- DiscardPolicy => 放弃最新提交的任务
- DiscardOldestPolicy => 放弃接下来要执行的任务
- CallerRuns => 任务被拒绝添加后,会用调用execute函数的上层线程去执行被拒绝的任务
有了基本的认识后,我们再看看Executors提供的几种线程池,就能够明白为什么阿里会强制禁止使用Executors创建线程池了
一、FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
很明显,FixedThreadPool的corePoolSize=maximunPoolSize ,且具体数值是由我们指定,也难怪叫做固定线程池,因为核心线程数等于固定线程数,而核心线程不会默认不会被回收处理,因此线程数量是固定的,接下来看看对于的工作队列LinkedBlockingQueue源码中如何设置容量
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
该队列中可以指定容量,但是默认为int的最大值2^31-1。看到这里我们就明白了,我们重新思考阿里编程规约的话,因为工作队列的容量太大,当线程数量大于核心线程数且使用默认工作队列长度时,任务会大量堆积到工作队列,很容易导致OOM。
二、SingleTheadPool
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
同样的,我们观察到corePoolSize=maximunPoolSize=1,单一线程池的线程数量固定,为一,也因此称为单一线程池。工作队列同样使用LinkedBlockingQueue,因此也可能堆积大量任务导致OOM
三、CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
在缓存线程池中,corePoolSize=0,maximumPoolSize=2^31-1,我们看下SynchronousQueue这个工作队列,这个队列比较特殊,该工作队列不保存任何任务被等待执行,而是直接提交给线程进行执行,容量可以理解为0
因此,如果创建的任务过多,会一直创建线程去执行任务,可能会导致OOM
四、ScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
分析过程很熟悉了,corePoolSize我们可以自己指定,同样maximumPoolSize=2^31-1,但是这个延迟工作队列DelayedWorkQueue我们没有看过,点进去他的源码
private static final int INITIAL_CAPACITY = 16;
private void grow() {
int oldCapacity = queue.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50%
if (newCapacity < 0) // overflow
newCapacity = Integer.MAX_VALUE;
queue = Arrays.copyOf(queue, newCapacity);
}
我又懂了,这个队列初始容量为16,容量满了后会进行扩容,每次扩容50%,最大值同样为int最大值,而且还是一个延迟队列。
五、总结
- 线程池工作原理是当线程数量小于核心线程数,则创建线程执行任务。当线程数量大于核心线程数,则任务进入工作队列。当工作队列已满,则创建线程执行任务,直到线程数量等于最大线程数
- FixedThreadPool和SingleTheadPool使用了LinkedBlockingQueue这个无界队列,可能会导致大量请求或任务堆放在队列中导致OOM
- CachedThreadPool和ScheduledThreadPool本身最大线程数maxmumPoolSize是int的最大值,可能创建过多的线程导致oom