首页 > 其他分享 >ThreadPoolExecutor keepAliveTime 含义

ThreadPoolExecutor keepAliveTime 含义

时间:2025-01-05 17:22:57浏览次数:1  
标签:销毁 队列 含义 keepAliveTime corePoolSize 线程 ThreadPoolExecutor

现象

在线上环境排查问题时,某个线程池在某个时间点新建线程达到设定的最大线程数 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

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 执行流程

  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 全局锁
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 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 的理解

源码中的注释

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

标签:销毁,队列,含义,keepAliveTime,corePoolSize,线程,ThreadPoolExecutor
From: https://www.cnblogs.com/cartooon/p/18653508

相关文章

  • 【Java并发编程线程池】 ForkJoinPool 线程池是什么 怎么工作的 和传统的ThreadPoolEx
    Java中的ForkJoinPool线程池是什么怎么工作的Java中的ForkJoinPool线程池是什么怎么工作的相比较于传统的线程池,ForkJoinPool线程池更适合处理大量的计算密集型任务,它的核心思想是将一个大任务拆分成多个小任务,然后将这些小任务分配给多个线程去执行,最后将这些小任务的......
  • html中<script> 标签中type值及其含义
    在HTML中的script标签中,type属性用于指定脚本的MIME类型,也即告诉浏览器该如何解释和处理脚本的内容。常用的type值以及它们的含义如下:1.type=“text/javascript”含义:指定脚本是JavaScript类型。这是早期的标准方式,现代浏览器默认都会将script标签中的内容当作J......
  • 拉链表,流⽔表以及快照表的含义和特点
    拉链表含义拉链表主要用于记录数据的历史变化情况。从数据结构角度看,它的每条记录都包含了一个实体(如客户、产品等)的关键信息以及两个时间戳字段,即起始日期(Start_Date)和结束日期(End_Date)。起始日期表示这条记录开始生效的时间,结束日期表示这条记录失效的时间。当数据初次录......
  • 解释下`(~~(Math.random()*(1<<24)))`的含义
    这段代码(~~(Math.random()*(1<<24)))在前端开发中可能用于生成一个随机整数。下面我们来分解这段代码,以更好地理解其含义:Math.random():这个函数返回一个[0,1)之间的随机浮点数,也就是说,它会返回一个大于等于0且小于1的随机小数。1<<24:这是一个位移运算。1左移24位,等......
  • Java面试要点97 - Java中ThreadPoolExecutor源码解析
    文章目录引言一、核心属性1.1状态与线程数量的原子控制1.2任务队列与工作线程组二、Worker线程包装类2.1Worker类的设计三、任务提交源码分析3.1execute方法实现3.2addWorker核心方法四、任务执行源码分析4.1runWorker方法实现4.2getTask方法分析五、线程池......
  • 28. Object 有哪些常用方法?大致说一下每个方法的含义
    java.lang.Object下面是对应方法的含义。clone方法保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常深拷贝也需要实现Cloneable,同时其成员变量为引用类型的也需要实现Cloneable,然后重写clone方法。finalize......
  • Android Systrace的部分Tag含义
    systrace的一些tag标签的含义和作用。1.CPU*(0-7)Kernel内核模块,可以查看各个CPU执行了什么进程任务。cpu信息的目录是/sys/devices/system/cpu,例如我的一加六老设备:OnePlus6:/sys/devices/system/cpu$lscore_ctl_isolatedcpu4cpuidleisolatedposs......
  • Delphi:含义、用途、过程和应用
    ​什么是德尔菲法?德尔菲法是一种用于预测和决策的结构化通信方法,涉及专家小组。该过程包括多轮问卷,专家提供匿名回答。这些回复被汇总并与小组共享,允许根据集体意见进行调整以达成共识。从选择专家开始,该方法通过多轮评论进行,直到达成共识。虽然它提供了一些优势,例如无......
  • 说说你对abbr标签的理解,它有什么含义?
    abbr标签,是HTML中用于表示缩写的标签,它有两个主要含义和作用:语义化:abbr标签明确告诉浏览器以及其他程序(例如屏幕阅读器)括起来的文本是一个缩写。这对于提升网页的可访问性和SEO都很有帮助。屏幕阅读器可以根据这个标签,以不同的方式朗读缩写,例如拼出完整的单词或提供预......
  • ntp-service unicast-server命令的含义
    ntp-serviceunicast-server 是网络时间协议(NTP)配置中的一项,用于在设备(如路由器或交换机)上设置NTP服务,以单播模式与特定时间服务器进行同步。以下是相关概念和配置说明:1.什么是单播模式(UnicastMode)单播模式是NTP的一种通信方式,设备直接向指定的NTP服务器发送时......