ThreadPoolExecutor keepAliveTime 含义

现象

在线上环境排查问题时,某个线程池在某个时间点新建线程达到设定的最大线程数 maximumPoolSize,后续流量降低后当前线程数仍未回落,仍然为最大线程数,阻塞队列中有任务,但是活跃线程数显著减少。

之前的认知

固有的认知中,线程池运行原理:java.util.concurrent.ThreadPoolExecutor#execute

  1. 线程池内部维护 corePoolSize 个线程
  2. 任务提交后,若核心线程都已被占用,则添加到阻塞队列
  3. 阻塞队列已满,则新建线程直到线程数到达 maximumPoolSize
  4. 若阻塞队列已满,并且线程数到达 maximumPoolSize,则执行拒绝策略
  5. 超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,进行销毁。

冲突

认知第五点中:超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,进行销毁。明显与现象不符。现象肯定没问题的,就是认知有问题了:超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,至少不会马上销毁。

现实与认知的问题

  1. 超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,会不会销毁?
  2. 销毁的时机是?
  3. 为什么线程池中大多为休眠线程?线程池的线程数仍为最大线程数?

重塑认知

答案都在源码内

ThreadPoolExecutor 执行任务流程

线程池使用 demo

csharp 复制代码
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 移除流程

源码上看,只有以下两个方法

bash 复制代码
java.util.concurrent.ThreadPoolExecutor#addWorkerFailed
java.util.concurrent.ThreadPoolExecutor#processWorkerExit

望文生义,addWorkerFailed 作用为添加 worker 后的失败补偿动作,可以忽略这个方法。

所以正常的销毁动作,肯定是在 processWorkerExit 中。

processWorkerExit 执行流程

使用场景

仅在java.util.concurrent.ThreadPoolExecutor#runWorker 中 finally 执行

而 runWorker 则是任务执行的底层方法,那么这意味着:任务执行完,满足某几个前提条件就会销毁线程。那么前提条件是什么呢?

runWorker 执行流程

  1. while 循环调用 java.util.concurrent.ThreadPoolExecutor#getTask 获取任务
    1. 获取到任务后,走真实执行任务流程,beforeExecute/run/afterExecute
    2. 获取不到任务,则到 processWorkerExit 执行

getTask 执行流程

  1. 使用当前 worker 数与核心线程数关系判定变量 timed
  2. 根据 timed 判定 timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take()

keepAliveTime 第一次出现,并且是用于在当前 worker 数大于核心线程数情况下从阻塞队列中获取元素。

那么,控制 processWorkerExit 执行的前提条件:当前 worker 数大于核心线程数,并且从阻塞队列经过 keepAliveTime 拿不到任务。

但这个前提条件明显跟现象不符,那肯定是 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 中被阻塞了,导致实际获取任务时间 > keepAliveTime。

workQueue.poll 执行流程(以 ArrayBlockingQueue 为例)

  1. 获取 ArrayBlockingQueue 全局锁
  2. 当队列元素个数 = 0, 则 await keepAliveTime 时间
  3. 队列元素个数 != 0,出队元素
  4. 释放 ArrayBlockingQueue 全局锁
csharp 复制代码
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 后,若当前队列为空,则执行销毁方法。

sql 复制代码
@startuml

"Thread" -> "BlockingQueue": pool task
"Thread" -> "BlockingQueue": get global ReentrantLock
alt get global ReentrantLock success
    alt BlockingQueue size = 0
        "Thread" -> "Condition": await keepAliveTime
        "BlockingQueue" -> "Thread": non task,execute processWorkerExit method
    else
        "BlockingQueue" -> "Thread": first task in queue
        "Thread" -> "Thread": keep execute task
    end
else 
    "Thread" -> "BlockingQueue": keep acquire ReentrantLock
end
@enduml

那么上述提到的两个问题

  1. 超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,会不会销毁?
  2. 销毁的时机是?
  3. 为什么线程池中大多为休眠线程?线程池的线程数仍为最大线程数?

就有了答案

  1. 超过 corePoolSize 部分的空闲线程,到达 keepAliveTime 后,有可能销毁,前提是拿到队列的全局锁。

  2. 销毁的时机是当前线程获取到队列全局锁,并且队列元素 = 0,并且 await 后队列元素仍然为 0

  3. 因为线上提交任务刚好够核心线程消费,并且残留少数任务在阻塞队列中。在并发情况下,大部分线程都 await,线程池只能新增 worker 处理了。

自言自语

怎么解决当前线程数 = 最大线程数,并且活跃线程较少的情况?

  1. 调高 corePoolSize ,使线程池不新增 corePoolSize 之外的线程。
  2. 调低 keepAliveTime & TimeUnit 的值,使休眠线程快速被销毁。

在商业开发的角度上,比较难精准实现。

  1. 业务发展速度很快, corePoolSize 在将来的一段时间内就不适合了。
  2. 加快休眠线程的销毁,意味着存在频繁新建线程的问题,会影响系统稳定性。

为什么 await keepAliveTime后不直接销毁?还尝试出队元素?

这就回到 java 线程与操作系统线程的映射关系。

线程模型有三种:一对一,多对一,一对多。java 在大多数平台上都是一对一。

  1. 如果直接销毁,核心线程处理不过来情况下,线程池会频繁销毁/新建线程,消耗系统的资源。
  2. 尝试出队元素,double check 线程池的负载,负载高则继续处理,负载较低则销毁线程,达到节省资源的目的。

keepAliveTime 的理解

源码中的注释

csharp 复制代码
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的博客

转载请注明出处:cartoonyu.github.io

相关推荐
张张张3121 分钟前
4.2学习总结 Java:list系列集合
java·学习
KATA~4 分钟前
解决MyBatis-Plus枚举映射错误:No enum constant问题
java·数据库·mybatis
xyliiiiiL20 分钟前
一文总结常见项目排查
java·服务器·数据库
shaoing21 分钟前
MySQL 错误 报错:Table ‘performance_schema.session_variables’ Doesn’t Exist
java·开发语言·数据库
Asthenia041223 分钟前
由浅入深解析Redis事务机制及其业务应用-电商场景解决超卖
后端
Asthenia041224 分钟前
Redis详解:从内存一致性到持久化策略的思维链条
后端
Asthenia041224 分钟前
深入剖析 Redis 持久化:RDB 与 AOF 的全景解析
后端
Apifox35 分钟前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
掘金一周42 分钟前
金石焕新程 >> 瓜分万元现金大奖征文活动即将回归 | 掘金一周 4.3
前端·人工智能·后端
uhakadotcom1 小时前
构建高效自动翻译工作流:技术与实践
后端·面试·github