网上介绍ThreadPoolExecutor的文章很多,这里不再累述。ThreadPoolExecutor是Doug lea的大作,但是只要是人写的代码都可能出现问题。
ThreadPoolExecutor是一个具有动态弹性的线程池,其会自动根据业务的请求量来动态伸缩其内的线程数目(当然判断的依据主要是corePoolSize、BlockingQueue的size与maximumPoolSize的大小关系来决策,对于线程池这样平台级的产物也只能按此标准了,想要按照业务自身特点的来控制线程的伸缩,需要继承ThreadPoolExecutor自己实现了),
简单说:
1、poolSize < corePoolSize 且线程池的状态为RUNNING时,产生新的worker线程处理任务。
2、poolSize = corePoolSize 时,将任务放入阻塞队列中,接着线程池会对volatile的runState进行double check,
进而判断刚刚加入队列的任务是否能继续呆在队列中等待处理(有可能刚加入队列,线程池就shutdown或者stop了)。
3、当队列满的时候,poolSize < maximumPoolSize, 线程池会增加线程来处理新的任务。最后当线程数达到
maximumPoolSize的时候,会根据拒绝策略来处理任务。
4、当队列中没有任务的时候,且线程池中存在空闲的线程(根据构建线程池传入的keepAliveTime作为到
BlockingQueue获取任务的超时时间),这样线程池会收缩空闲线程到corePoolSize。
这是ThreadPoolExecutor工作的基本原理。我们知道线程创建有一定的开销,这样我们才使用池化的构件思想来处理这
种昂贵的对象资源,所以我们应该更加关注对已有线程的释放,如果释放不当,可能导致在需要的时候又会重复重新创建
已经创建过的线程,形成不必要的开销。另外,如果线程池的伸缩策略不稳定,必然会影响业务的稳定性。那么,
ThreadPoolExecutor真的能把空闲的线程降低到corePoolSize吗?答案是半对半错的,在某些情况下可以,在某些情况
下会低于该值,乃至于降低到零。问题的根源在于ThreadPoolExecutor的getTask方法的实现:
Runnable getTask() {
for (;;) {
try {
int state = runState;
if (state > SHUTDOWN)
return null;
Runnable r;
if (state == SHUTDOWN) // Help drain queue
r = workQueue.poll();
else if (poolSize > corePoolSize || allowCoreThreadTimeOut)
r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
else
r = workQueue.take();
if (r != null)
return r;
if (workerCanExit()) {
if (runState >= SHUTDOWN) // Wake up others
interruptIdleWorkers();
return null;
}
// Else retry
} catch (InterruptedException ie) {
// On interruption, re-check runState
}
}
}
第10行代码在某些场景下是有缺陷的,这里不讨论降低到corePoolSize的正常情况,我们探讨下另外的场景。
假设线程池的corePoolSize设置为3,阻塞对列为长度为3,maximumPoolSize的值为4,keepAliveTime为1秒,
线程池一共接收了7个任务,线程池的快照如下:
假设当R1、R2、R3被处理完后(R7仍然在处理中),那么corePoolSize线程去队列中获取R4、R5、R6任务进行处理,如下:
这样当R4、R5、R6、R7任务都被处理完后,所有的线程(这里为4)都会调用getTask方法到队列中获取新的任务,如
下:
这样,当所有的线程都执行到
else if (poolSize > corePoolSize || allowCoreThreadTimeOut)
r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
的时候,poolSize > corePoolSize 都为true(因为poolSize与corePoolSize都是volatile的,每个线程都能获取主存
的最新值),那么当在队列中等待keepAliveTime后(这里为1秒),所有的线程都将从队列中得到一个null值的返回,这样在
后面的逻辑中,都将以null值作为getTask的返回。这样每个worker线程都会退出自己的main loop,进而退出worker
的run方法(每个worker自身也是一个Runnable),最终线程池中的线程都降到零,而不是corePoolSize=3。
同时,使用ThreadPoolExecutor还要注意权衡corePoolSize、BlockingQueue队列的大小以及maximumPoolSize
三者的关系,否则使用ThreadPoolExecutor不当会给你的应用带来意想不到的负面结果。