现象
在线上环境排查问题时,某个线程池在某个时间点新建线程达到设定的最大线程数 maximumPoolSize,后续流量降低后当前线程数仍未回落,仍然为最大线程数,阻塞队列中有任务,但是活跃线程数显著减少。
之前的认知
固有的认知中,线程池运行原理:java.util.concurrent.ThreadPoolExecutor#execute
- 线程池内部维护 corePoolSize 个线程
- 任务提交后,若核心线程都已被占用,则添加到阻塞队列
- 阻塞队列已满,则新建线程直到线程数到达 maximumPoolSize
- 若阻塞队列已满,并且线程数到达 maximumPoolSize,则执行拒绝策略
- 超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,进行销毁。
冲突
认知第五点中:超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,进行销毁。明显与现象不符。现象肯定没问题的,就是认知有问题了:超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,至少不会马上销毁。
现实与认知的问题
- 超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,会不会销毁?
- 销毁的时机是?
- 为什么线程池中大多为休眠线程?线程池的线程数仍为最大线程数?
重塑认知
答案都在源码内
ThreadPoolExecutor 执行任务流程
线程池使用 demo
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 10, 100, TimeUnit.MINUTES, new ArrayBlockingQueue<>(1000));threadPoolExecutor.execute(() -> System.out.println("print in thread"));
执行流程
java.util.concurrent.ThreadPoolExecutor#execute
流程就是之前认知中 1 - 4 点,在第三点中蕴含一个重要变量:java.util.concurrent.ThreadPoolExecutor#workers
,这个就是ThreadPoolExecutor 管理线程的对象
workers 移除流程
源码上看,只有以下两个方法
java.util.concurrent.ThreadPoolExecutor#addWorkerFailed
java.util.concurrent.ThreadPoolExecutor#processWorkerExit
望文生义,addWorkerFailed 作用为添加 worker 后的失败补偿动作,可以忽略这个方法。
所以正常的销毁动作,肯定是在 processWorkerExit 中。
processWorkerExit 执行流程
使用场景
仅在java.util.concurrent.ThreadPoolExecutor#runWorker
中 finally 执行
而 runWorker 则是任务执行的底层方法,那么这意味着:任务执行完,满足某几个前提条件就会销毁线程。那么前提条件是什么呢?
runWorker 执行流程
- while 循环调用
java.util.concurrent.ThreadPoolExecutor#getTask
获取任务- 获取到任务后,走真实执行任务流程,beforeExecute/run/afterExecute
- 获取不到任务,则到 processWorkerExit 执行
getTask 执行流程
- 使用当前 worker 数与核心线程数关系判定变量 timed
- 根据 timed 判定
timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take()
keepAliveTime 第一次出现,并且是用于在当前 worker 数大于核心线程数情况下从阻塞队列中获取元素。
那么,控制 processWorkerExit 执行的前提条件:当前 worker 数大于核心线程数,并且从阻塞队列经过 keepAliveTime 拿不到任务。
但这个前提条件明显跟现象不符,那肯定是 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)
中被阻塞了,导致实际获取任务时间 > keepAliveTime。
workQueue.poll 执行流程(以 ArrayBlockingQueue 为例)
- 获取 ArrayBlockingQueue 全局锁
- 当队列元素个数 = 0, 则 await keepAliveTime 时间
- 队列元素个数 != 0,出队元素
- 释放 ArrayBlockingQueue 全局锁
public E poll(long timeout, TimeUnit unit) throws InterruptedException {long nanos = unit.toNanos(timeout);final ReentrantLock lock = this.lock;lock.lockInterruptibly();try {while (count == 0) {if (nanos <= 0)return null;nanos = notEmpty.awaitNanos(nanos);}return dequeue();} finally {lock.unlock();}}
真相
从workQueue.poll 执行流程中,能明显看到线程 await 的前提是获取到队列的全局锁,并且队列元素 = 0。
整理一遍就是:
当线程获取到队列全局锁,并且当前队列为空,await keepAliveTime 后,若当前队列为空,则执行销毁方法。
@startuml"Thread" -> "BlockingQueue": pool task
"Thread" -> "BlockingQueue": get global ReentrantLock
alt get global ReentrantLock successalt BlockingQueue size = 0"Thread" -> "Condition": await keepAliveTime"BlockingQueue" -> "Thread": non task,execute processWorkerExit methodelse"BlockingQueue" -> "Thread": first task in queue"Thread" -> "Thread": keep execute taskend
else "Thread" -> "BlockingQueue": keep acquire ReentrantLock
end
@enduml
那么上述提到的两个问题
- 超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,会不会销毁?
- 销毁的时机是?
- 为什么线程池中大多为休眠线程?线程池的线程数仍为最大线程数?
就有了答案
-
超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,有可能销毁,前提是拿到队列的全局锁。
-
销毁的时机是当前线程获取到队列全局锁,并且队列元素 = 0,并且 await 后队列元素仍然为 0
-
因为线上提交任务刚好够核心线程消费,并且残留少数任务在阻塞队列中。在并发情况下,大部分线程都 await,线程池只能新增 worker 处理了。
自言自语
怎么解决当前线程数 = 最大线程数,并且活跃线程较少的情况?
- 调高 corePoolSize ,使线程池不新增 corePoolSize 之外的线程。
- 调低 keepAliveTime & TimeUnit 的值,使休眠线程快速被销毁。
在商业开发的角度上,比较难精准实现。
- 业务发展速度很快, corePoolSize 在将来的一段时间内就不适合了。
- 加快休眠线程的销毁,意味着存在频繁新建线程的问题,会影响系统稳定性。
为什么 await keepAliveTime后不直接销毁?还尝试出队元素?
这就回到 java 线程与操作系统线程的映射关系。
线程模型有三种:一对一,多对一,一对多。java 在大多数平台上都是一对一。
- 如果直接销毁,核心线程处理不过来情况下,线程池会频繁销毁/新建线程,消耗系统的资源。
- 尝试出队元素,double check 线程池的负载,负载高则继续处理,负载较低则销毁线程,达到节省资源的目的。
keepAliveTime 的理解
源码中的注释
when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.当线程数大于 Core 数时,这是多余的空闲线程在终止之前等待新任务的最长时间。
之前以为是线程数大于 Core 数时,空闲线程的存活时间。过了 keepAliveTime 就执行销毁。
现在认识到:线程数大于 Core 数时,空闲线程的存活时间 >= keepAliveTime (没获取到队列锁的情况下),并且销毁前 double check 是否有任务,没有才执行销毁。
本文首发于cartoon的博客
转载请注明出处:https://cartoonyu.github.io