ThreadPoolExecutor 线程池 —— 空闲的非核心线程销毁
目录
前言
背景
正文开始
痛定思痛
总结
前言
线程池大家其实都很熟悉,在多线程场景下也经常用到,其中涉及到的一些知识也是非常重要的。比如:如何根据项目去配置线程池的核心参数、如何设计一个动态线程池。校招面试中也经常问到线程池的工作机制以及具体的核心参数等概念。
荔枝最近面试猫眼也被问到一个有关线程池的问题,考察的点还是比较详细的,直击痛点,主要考察有没有阅读过源码以及对于线程池的理解程度,接下来荔枝会着重分享一下自己的面试过程,以及后面的复盘。
背景
猫眼娱乐二面。顺便说一下荔枝的面试观感,感觉猫眼的面试难度挺大的,一面无八股纯项目+手撕一道算法+手撕策略模式(1h+),二面就是一些比较深的八股和项目、手撕题和场景题拷打(1h)。不过面试官的态度很友好,循循善诱。当时聊完项目的细节(问的非常细),然后在全面问一些基础的技术的时候,我们聊到了线程池。
线程池?那你可问到我的点子上了,不就那几个参数和工作机制么,随便拷打,分分钟八股吟唱~
正文开始
面试官对我的回答非常满意。
“好,看来你对线程池比较熟悉呀,我再问细一点哈,你刚刚那个空闲线程的最大存活时间对吧,那你知道线程池内部怎么判断这个空闲线程要被回收掉呢?”
“这块源码有点忘了,但我猜想可能会有一个定时的线程的设计?噢不那不对...”
有点懵说实话,荔枝确实源码有点忘了,我错了面试官,刚刚不该狂妄的呜呜呜呜~
痛定思痛
还是乖乖看一下源码,首先我问了一下 gpt 哈,它说定位到这个方法可以看到,仔细分析一下:
// Worker 线程获取任务的入口
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
//死循环 简单理解就是从 BlockingQueue 中拿任务-执行任务
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 判断线程池是否处于关闭状态且 workQueue 任务队列是空的 | 线程池处于终止状态(此时不会处理任务且不会接受新的任务)
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
// 数一下当前工作线程的个数
int wc = workerCountOf(c);
// 判断是否设置了核心线程超时回收 || 当前有非核心线程
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 超过最大线程数 || 设置了核心线程超时并且任务队列此时是空的拿不到任务 || 有非核心线程
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
// 执行任务,take是此时由于没有设置线程超时回收,就阻塞等待任务;poll 方法是用来判断是否获取任务超时,超时的时间其实就是我们配置的非核心线程的最大存活时间;
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
看来上面源码还是讲不清楚,到底咋终止非核心线程的呀?compareAndDecrementWorkerCount() 方法么?这个方法看起来只是在线程池处于终止状态时通过原子的方式减少线程池的线程数。显然不是靠他来回收空闲的非核心线程,这时候我觉得得从 Worker 这个类来看。


我们知道线程池其实内部就是将普通的线程包装成 Worker 线程,那么当线程执行任务其实也就是调用 run 方法,而在 run 方法内部其实会调用 getTask() 去尝试获取任务。这就和上面的 poll 串起来了:

还是看图比较容易,这里的 poll() 调用很明显只有两种情况:
- 允许核心线程超时回收
- 存在非核心线程
这就可以理解了,如果此时满足上面两个条件任意,一般是第二个条件吧,此时非核心线程一直 poll() 拿阻塞队列中的任务,但是出现超时了,其实就是队列中没有任务,这不就是空闲么。拿到的这个 r 也就是 null 返回出去到 runWorker 里面,自然就不进入循环啦,线程销毁回收。
挺好的,又收获一个知识点,这里的设计感觉好优雅哈哈哈,充分利用了阻塞队列的特性。
这里 take() 的存在是为了保护核心线程,只要核心线程没有被设置允许超时回收,那么就一直会阻塞等待任务的到来。而且如果设置了允许超时回收,即使当前线程池中的核心线程数<最大线程数,当线程池中没有任务的时候,核心线程也会被销毁。
总结
线程池这块还是感觉有很多地方没有弄得很清楚,同时线程池也有一些其它的变种,比如亲缘线程池。这一块的内容分享荔枝也会尽快梳理总结起来的。如果上述文章有什么理解不对的地方,也请各位大佬批评指正~
