乐趣区

关于线程池你不得不知道的一些设置

看完我上一篇文章「你都理解创建线程池的参数吗?」之后,当遇到这种问题,你觉得你完全能够唬住面试官了,50k 轻松到手。殊不知,要是面试官此刻给你来个反杀:
初始化线程池时可以预先创建线程吗?线程池的核心线程可以被回收吗?为什么?
如果此刻你一脸懵逼,这个要慌,问题很大,50k 马上变 5k。

有细心的网友早就想到了这个问题:

在 ThreadPoolExecutor 线程池中,还有一些不常用的设置。我建议如果您在应用场景中没有特殊的要求,就不需要使用这些设置。
初始化线程池时可以预先创建线程吗?
prestartAllCoreThreads
初始化线程池时是可以预先创建线程的,初始化线程池后,再调用 prestartAllCoreThreads() 方法,即可预先创建 corePoolSize 数量的核心线程,我们看源码:
public int prestartAllCoreThreads() {
int n = 0;
while (addWorker(null, true))
++n;
return n;
}
private boolean addWorker(Runnable firstTask, boolean core) {
// ..
}
addWorker 方法目的是在线程池中添加任务并执行,如果 task 为空,线程获取任务执行时调用 getTask() 方法,该方法从 blockingQueue 阻塞队列中阻塞获取任务执行,因此线程不会释放,留存在线程池中,如果 core=true,说明任务只能利用核心线程来执行。
所以该方法会在线程池总预先创建没有任务执行的线程,数量为 corePoolSize。
下面我们测试一下:

从测试结果来看,线程池中已经预先创建了 corePoolSize 数量的空闲线程。
prestartCoreThread
prestartCoreThread() 同样可以预先创建线程,只不过该方法只会与创建 1 条线程,我们来看源码:
public boolean prestartCoreThread() {
return workerCountOf(ctl.get()) < corePoolSize &&
addWorker(null, true);
}
从方法源码可知,如果此时工作线程数量小于 corePoolSize,那么就调用 addWorker 创建 1 条空闲核心线程。
下面我们测试一下:

从测试结果来看,线程池中已经预先创建了 1 条空闲线程。
线程池的核心线程可以被回收吗?
你可能会想到将 corePoolSize 的数量设置为 0,从而线程池的所有线程都是“临时”的,只有 keepAliveTime 存活时间,你的思路也许时正确的,但你有没有想过一个很严重的后果,corePoolSize= 0 时,任务需要填满阻塞队列才会创建线程来执行任务,阻塞队列有设置长度还好,如果队列长度无限大呢,你就等着 OOM 异常吧,所以用这种设置行为并不是我们所需要的。
有没有什么设置可以回收核心线程呢?
allowCoreThreadTimeOut
ThreadPoolExecutor 有一个私有成员变量:
private volatile boolean allowCoreThreadTimeOut;
如果 allowCoreThreadTimeOut=true,核心线程在规定时间内会被回收。
上面我也说了,当线程空闲时会从 blockingQueue 阻塞队列中阻塞获取任务执行,所以我们来看看是保证核心线程不被销毁的,我们直接定位到源码部位:
java.util.concurrent.ThreadPoolExecutor#getTask:
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
这里的关键值 timed,如果 allowCoreThreadTimeOut=true 或者此时工作线程大于 corePoolSize,timed=true,如果 timed=true,会调用 poll() 方法从阻塞队列中获取任务,否则调用 take() 方法获取任务。
下面我来解释这两个方法:

poll(long timeout, TimeUnit unit):从 BlockingQueue 取出一个任务,如果不能立即取出,则可以等待 timeout 参数的时间,如果超过这个时间还不能取出任务,则返回 null;
take():从 blocking 阻塞队列取出一个任务,如果 BlockingQueue 为空,阻断进入等待状态直到 BlockingQueue 有新的任务被加入为止。

到这里,我们就很好地解释了,当 allowCoreThreadTimeOut=true 或者此时工作线程大于 corePoolSize 时,线程调用 BlockingQueue 的 poll 方法获取任务,若超过 keepAliveTime 时间,则返回 null,timedOut=true,则 getTask 会返回 null,线程中的 runWorker 方法会退出 while 循环,线程接下来会被回收。
下面我们测试一下:

可以看到,核心线程被回收了。
写在最后
后面我会单独写一篇从源码的角度深度解读线程池的运行原理,敬请期待。
另外,我公众号也改名字了,这个公众号的内容源自于我的博客,我的博客域名是 objcoding,所以干脆公众号就叫这个名字了,但是很多网友误以为我是 objective- c 开发的,宝宝心里苦啊,其实这个域名的是面向对象编程的意思,即在 Java 狗眼里一切皆对象。
相信小伙伴们上学时,也有被老师“忽悠”去当科代表的经历吧,比如老师跟你说:「当哪科科代表,哪科成绩就会上升」,「你哪科弱,就当哪科科代表,最好了」
其实当科代表,最重要的一个条件是对这门学科有着浓厚的兴趣与热爱。
所以,从今天开始,我厚着脸皮,当一次 Java 科代表。

退出移动版