Java

线程池打满以后,应该先查队列、拒绝策略还是慢任务?

线程池打满只是执行层红灯,不等于根因就在参数。把队列堆积、慢任务、下游等待、数据库链路和 CPU / GC 放在同一条排查链里看,才更容易找到真正拖慢 worker 的地方。

  • Java
  • 线程池
  • 并发
  • 性能排查
  • 线上问题
18 分钟阅读

线程池打满不是一个配置名词,而是一种执行层拥塞信号。监控里看到 active 顶满、queue 往上爬、reject 开始冒出来时,大家很容易立刻去调 maximumPoolSize、换拒绝策略,或者把队列再垫高一点。

这些动作有时能争取时间,但更常见的情况是:线程不是不够,而是被慢任务、下游等待、数据库链路、CPU 热点或 Full GC 抖动占住了。

所以这篇文章不重复解释 corePoolSizemaximumPoolSize 和拒绝策略的基础概念,而是直接回答一个更像线上排障的问题:

线程池打满以后,先看哪里最容易接近根因,怎样避免一上来就改参数、扩线程、清队列这种高概率误判?

排查时更实用的顺序是:先确认队列是不是持续堆积,再判断是不是慢任务;接着排查下游阻塞、数据库链路、CPU 热点,最后再看 GC 有没有在放大问题。

要是眼前还是“接口慢,但 CPU、GC、数据库都没明显高”,先回到 接口很慢,但 CPU、GC、数据库都正常,隐藏等待点可能在哪?,把等待链真正落点找出来。这里聚焦已经明显出现线程池打满、队列堆积、拒绝增多这类执行层拥塞信号的现场。


一、参数调整通常不是第一刀

线程池打满时,很多团队的第一反应通常有三种:

  • maximumPoolSize 调大
  • 把队列改大
  • 直接换一个更“激进”的拒绝策略

这些动作不是永远不能做,但如果它们出现在排查的第一步,往往意味着方向已经偏了。

因为线程池会被打满,本质上只有几种可能:

  • 任务进来的速度突然变快了
  • 任务执行得变慢了
  • 任务执行线程被别的资源卡住了
  • 线程虽然很多,但真正有效执行并不多
  • 系统整体已经被 CPU 或 GC 拖慢,线程池只是一起表现异常

也就是说,线程池的“满”既可能是入口压力问题,也可能是执行效率问题,还可能是别的组件先出故障后传导到线程池。

这也是为什么线程池文章如果只讲参数,往往只能解决“平时怎么配”,却解决不了“出事后怎么查”。真正值钱的是排查顺序。


二、先确认现场属于哪一类拥塞

不要一看到接口变慢就直接跳到线程池参数。可以借这张表分一下型,确认自己是不是已经进入执行层拥塞场景。

你现在看到的现象更像什么问题下一步更适合看什么
活跃线程打满、队列持续增长、拒绝开始出现典型线程池拥塞继续看本文
队列不长,但任务明显变慢更像伪正常 / 隐性等待线程池队列不长但任务还是慢,常见瓶颈在哪里?
异步任务、MQ 消费、补偿任务越堆越多更像异步 backlog 主要链路异步任务越堆越多,问题常常不在异步本身
Tomcat busy threads 很高,但业务线程池不一定同步恶化更像入口线程 vs 执行线程分诊Tomcat busy threads 很高,和业务线程池 backlog 是一回事吗?
CPU 不高、GC 正常、数据库也不高,但接口已经明显慢更像等待链还没落点接口很慢,但 CPU、GC、数据库都正常,隐藏等待点可能在哪?
获取连接变慢、连接池 pending 上升、事务型接口一起慢更像数据库连接池等待链线程池和数据库连接池的容量,为什么要一起做预算?

如果第一行最像现场,就继续把执行层拥塞往下拆;如果后面几行更贴近,就先去对应的等待链处理,别把所有并发等待都硬塞进“线程池打满”这个名字里。

同样叫线程池打满,实际线上至少有三种完全不同的形态。

1. 活跃线程打满,但队列没怎么涨

这通常意味着:

  • 线程池本身已经在持续高并发执行
  • 任务可能偏 CPU 密集
  • 或者线程数本来就偏小
  • 也可能是任务执行快,但流量突然抬高

这类场景里,重点往往不是先看拒绝策略,而是看 CPU、任务执行时间、入口流量变化

2. 活跃线程打满,队列也持续上涨

这是最常见、也最值得警惕的一种。

它通常意味着:

  • 当前处理能力已经明显低于任务进入速度
  • 线程没有空出来,后续任务开始排队
  • 真正的问题往往在“任务为什么变慢了”

这时优先级最高的通常不是拒绝策略,而是 慢任务、下游阻塞、数据库链路

3. 队列很长,但线程数并没有明显扩起来

这类情况很容易出现在:

  • 使用无界队列
  • maximumPoolSize 看起来很大,但实际上根本轮不到生效
  • 核心线程数偏小,队列先把任务全吞了

这时更该确认的不是队列表面上是不是平静,而是:

  • 队列是不是把问题藏起来了
  • 线程池是不是根本没有把扩线程机制用起来
  • 你的排队策略是不是已经在拖慢整个服务

所以第一步不要急着猜根因,先把你看到的“满”分清形态。


三、把排查顺序固定在执行链上

这是这篇文章最核心的部分。

线程池打满以后,我更建议按下面这条顺序查,而不是想到什么查什么:

  1. 先看队列:到底是在瞬时抖动,还是持续堆积
  2. 再看慢任务:单个任务是不是执行时间变长了
  3. 再看下游阻塞:HTTP、RPC、缓存、消息、文件 IO 是否在卡线程
  4. 再看数据库:慢 SQL、长事务、连接池等待是否在拖任务
  5. 再看 CPU:线程是不是都在忙计算、死循环、锁竞争
  6. 最后看 GC:是不是 GC 抖动把执行线程整体拖慢了

这个顺序的好处在于:它是从线程池最直接可见的表现,一层层回推到更底层的真实瓶颈。


四、从队列判断:是瞬时高峰,还是处理能力真的掉了

线程池打满以后,队列是最先该看的指标之一。

因为队列能回答一个非常关键的问题:

现在是“任务一下子来多了”,还是“任务开始处理不动了”?

重点看 3 个信号

  • 队列长度是瞬时冲高后回落,还是持续增长不回落
  • 队列增长是否和流量高峰同步
  • 队列上涨时,任务执行时间是否也同步变长

如果是瞬时冲高后很快回落

通常更像:

  • 突发流量
  • 定时任务批量触发
  • 某个批处理窗口集中提交任务

这时不一定是严重故障,更像是线程池削峰能力不够,或者业务峰值本来就没有设计缓冲。

如果是持续增长,而且不回落

这往往更值得紧张,因为它通常说明:

  • 当前任务处理速度长期低于进入速度
  • 线程池已经不是“有点忙”,而是“开始积债”
  • 如果继续放任,后面会从线程池问题演变成 RT 抖动、超时、拒绝、内存压力

队列这里最容易出现的误判

  • 误判 1:队列大说明系统稳。 实际上很多时候只是问题被藏起来了。
  • 误判 2:队列涨了就一定是流量太大。 也可能是单个任务突然变慢。
  • 误判 3:把队列改更大就是优化。 很多时候只是让故障延后、放大。

所以看队列不是为了决定“要不要加容量”,而是为了判断 系统是在抖,还是已经处理不动了。


五、第二步查慢任务:线程池打满时,最常见的根因往往就在这里

如果线程池活跃线程打满,同时队列持续增长,我通常会优先怀疑 任务本身变慢了

因为线程池被打满,本质上就是线程长时间不释放。线程为什么不释放?最常见的答案不是配置错,而是任务跑得比平时慢了。

先确认这些问题

  • 任务平均执行时间是否明显上涨
  • 是所有任务都变慢,还是某一类任务特别慢
  • 是最近发布后才出现,还是流量变化后出现
  • 是否存在某条固定业务路径把线程长期占住

慢任务常见长相

  • 批量处理单次吞太多数据
  • 某个循环里做了过重计算
  • 同步调用多个下游接口,串行执行太久
  • 任务里混入了慢 SQL、远程调用、文件操作
  • 降级或重试逻辑让一次任务实际执行了多轮

这一步最值得做的事

  • 看任务耗时监控或埋点
  • 抽样日志,确认慢在哪个阶段
  • 如果没有监控,至少先把任务拆成几个阶段看时间分布

这里不要急着只盯线程池线程数,要盯“线程被什么事情占住了这么久”。


六、第三步查下游阻塞:很多线程池问题,其实是被外部依赖拖慢的

线程池里的线程不一定在忙,也可能只是在等。

这是很多排查会忽略的一点。表面上看,线程都满了;实际上这些线程可能并没有做多少真正的计算,而是卡在:

  • HTTP 调用
  • RPC 调用
  • Redis 操作
  • 消息发送或消费
  • 磁盘 / 文件 IO
  • 第三方接口超时等待

这类问题的典型特征

  • 活跃线程很高,但 CPU 并不高
  • 线程栈里大量线程停在网络等待、socket read、future get、远程调用框架
  • 任务耗时变长,但业务代码本身不算重
  • 某个依赖 RT 抬升、超时率增加后,线程池开始堆积

为什么这一步很关键

因为你如果在这时只盯线程池参数,往往会做出两个错误动作:

  • 多开线程,结果把更多请求打到已经变慢的下游
  • 把队列调大,结果让更多请求一起排队等超时

这类问题的真实根因往往是:

不是线程池扛不住,而是线程池在替下游排队。

所以线程池打满后,下游依赖的 RT、错误率、超时率 一定要同步看。


七、第四步查数据库:慢 SQL、长事务、连接池等待,都会把线程池一起拖住

线程池问题和数据库问题,在线上几乎总是互相牵连的。

如果线程池里的任务依赖数据库,那下面这些问题都会直接传导成线程池堆积:

  • 慢 SQL
  • 锁等待
  • 长事务
  • 数据库连接池等待
  • 数据库实例整体变慢

典型传导链 usually 是这样

  • SQL 变慢或锁住
  • 单次任务执行时间拉长
  • 线程更久不释放
  • 活跃线程持续打满
  • 后续任务进入队列
  • 队列越积越长
  • 接口 RT 和超时开始一起抬升

这一层重点要看什么

  • 最近是否有慢 SQL 激增
  • 是否存在长事务、锁等待
  • 数据库连接池是否也出现等待或耗尽
  • 数据库 CPU / IO / 活跃连接数是否异常

如果这一步发现数据库链路异常,线程池就不要再孤立看了。很多时候线程池只是第一个把问题“显出来”的地方,真正慢的是数据库链路。

更完整的排查可以继续看这些文章:


八、第五步查 CPU:确认线程是真的在忙,还是只是看起来很忙

当慢任务、下游阻塞、数据库都还不能解释问题时,就该开始看 CPU 层面了。

因为还有一类线程池打满,是线程真的在“重度计算”或“无效消耗”。

常见场景包括

  • 死循环或空轮询
  • JSON 序列化 / 反序列化过重
  • 加解密、压缩、规则计算太重
  • 异常风暴导致大量栈构造和日志格式化
  • 锁竞争 / 自旋带来额外 CPU 开销

这类问题常见信号

  • 活跃线程打满
  • CPU 同时飙高
  • 吞吐并没有同步提升
  • top -Hp / jstack 能看到热点线程栈反复停在相同代码路径

这一步本质上是在回答:

线程是不是都在干活?如果是,那到底在干什么活?

如果线程池打满同时伴随 CPU 异常,优先去看《Java CPU 飙高怎么排查:一套线上定位顺序》,两者往往是同一个问题的两个表现面。


九、第六步再看 GC:GC 更像放大器,而不是线程池问题的第一起点

GC 当然也可能把线程池拖慢,但我不建议把它放在第一步查。

原因很简单:在线上实际场景里,GC 更常见的角色是放大器,而不是最初的起火点。

比如:

  • 队列堆积导致对象大量滞留,内存压力上涨
  • 大批任务同时排队和执行,引发分配速率异常
  • 业务本身已有内存问题,线程池堆积又进一步放大停顿

这时候你会看到:

  • GC 次数增多
  • 停顿时间抬升
  • 线程池处理能力进一步下降
  • 接口超时更严重

为什么不要一上来就把锅甩给 GC

因为很多人一看到 RT 变差、线程池堆积、GC 次数变多,就会立刻得出结论:“是不是 JVM 参数不行”。

但真实情况很可能是:

  • 先有慢任务 / 下游阻塞
  • 再有队列积压
  • 最后才有 GC 压力被放大

所以 GC 这一步要看,但更适合放在前面几层查过之后再判断因果。

如果这一步有明显异常,再去接《Full GC 频繁怎么办:先判断是不是内存泄漏》。


十、那拒绝策略要不要查?要,但它通常不是第一优先级

题目里提到拒绝策略,是因为很多人在线程池打满时第一反应就是去看它。

这不是完全错,但优先级通常没那么高。

为什么

因为拒绝策略决定的是:

  • 当系统已经顶不住时,怎么失败

它回答的不是:

  • 系统为什么顶不住

如果你一上来就纠结 AbortPolicyCallerRunsPolicyDiscardPolicy,很容易陷入一个错位排查:

  • 你在研究“症状最后怎么表现”
  • 却还没搞清“问题最早怎么产生”

拒绝策略真正该怎么看

更合理的问题是:

  • 当前拒绝是不是刚开始出现,还是已经持续很久
  • 拒绝是否把问题暴露得足够清楚
  • CallerRunsPolicy 是否把主请求线程一起拖慢了
  • DiscardPolicy 是否让业务悄悄丢任务却不自知

所以拒绝策略更像是 损失控制和失败方式设计 的一部分,而不是根因排查的起点。


十一、这篇文章和隐藏等待 / CPU 高 / 连接池链的边界

如果你已经确认线程池有拥塞信号,但还拿不准下一步该切哪条线,可以直接按下面这组边界分支。

十二、线程池打满时,最容易出现的几个误判

误判 1:先调大线程数

如果根因是慢任务、下游超时或数据库卡顿,多开线程通常只会把压力继续放大。

误判 2:先调大队列

这通常不是优化,而是延迟报警、扩大积压、增加内存风险。

误判 3:看见拒绝就只盯拒绝策略

拒绝只是最后一道防线,不是最早的问题来源。

误判 4:把线程池问题和 CPU、数据库、GC 拆开看

真实线上故障里,这几个方向经常就是一条链上的不同位置。

误判 5:只看线程池监控,不看任务耗时

线程池满不满是结果,任务耗时为什么变长才是关键。

误判 6:只看单时刻截图,不看趋势

很多问题不是“现在满了”,而是“已经连续 20 分钟恢复不过来”。趋势比瞬时值更重要。


十三、一个更实用的现场排查清单

如果线上真的遇到线程池打满,我一般会这样快速过一遍:

第 1 步:看线程池本身

  • 活跃线程数是否持续打满
  • 队列长度是否持续增长
  • 拒绝次数是否开始上升
  • 是单实例异常还是整个集群一起异常

第 2 步:看任务耗时

  • 平均执行时间是否明显上涨
  • 哪类任务最慢
  • 最近是否有新逻辑、新依赖、新批任务上线

第 3 步:看下游依赖

  • HTTP / RPC / Redis / MQ / 文件 IO 是否变慢
  • 超时率、错误率是否同步升高
  • 是否存在重试放大

第 4 步:看数据库

  • 慢 SQL 是否激增
  • 是否存在锁等待、长事务
  • 数据库连接池是否同时出现等待

第 5 步:看 CPU 和线程栈

  • CPU 是否同步升高
  • 热点线程在做什么
  • 是否存在死循环、锁竞争、异常风暴

第 6 步:最后再看 GC

  • GC 次数和停顿是否明显异常
  • GC 是起因,还是因为积压而被放大

这个顺序的目标不是“覆盖所有可能性”,而是尽量让你先排最常见、最能解释现象的方向。


十四、FAQ:线程池打满后最容易继续追问的几个问题

1. 线程池参数文章和这篇故障排查文章是什么边界?

传统参数文章更关心 corePoolSizemaxPoolSize 和拒绝策略怎么配;这篇更关心已经看到 active、queue、reject 变差之后,先判断执行层为什么会拥塞。换句话说,本文讨论的是线程池已经报警之后,现场该怎么收敛,不是日常配参手册。

2. 线程池打满以后,应该先看 queue、active,还是先看拒绝策略?

先用 queue 的变化趋势判断是不是已经形成持续积压,再结合 active 判断线程是否长期不释放。拒绝策略当然要看,但它更多是在系统已经扛不住之后定义怎么失败,不是最早的根因判断入口。

3. 线程池打满一定是线程数太小吗?

不一定,而且很多时候不是。更常见的情况是任务变慢、下游阻塞、数据库卡顿、连接池等待或 CPU 热点,导致线程长时间不释放。

4. 线程池打满但数据库指标不高,还要不要继续查数据库链?

要,尤其是获取连接变慢、事务型接口更慢、pending 更早抬升时。数据库没有整体打满,不代表连接池等待、长事务和锁等待没有在拖慢 worker 线程。

5. 线程池打满和“隐藏等待点”最大的区别是什么?

那篇处理的是“资源图不高,但慢点还没落到具体等待链上”;本文处理的是“已经看到 worker 执行层拥塞信号后,按 queue -> 慢任务 -> 下游 -> 数据库 -> CPU -> GC 的顺序继续收敛”。前者先帮你找等待链落点,后者再解释线程池这层为什么已经开始堵。

6. 线程池打满后,什么时候该继续看容量规划而不是继续停在故障排查?

当你已经确认这不是某次慢 SQL、下游抖动、CPU 热点或 GC 放大,而是长期并发预算和资源边界就设计错了,才更适合继续看 线程池和数据库连接池的容量,为什么要一起做预算?。容量治理是治理收口,不替代本文的故障判断顺序。

十五、看到线程池报警后,我一般会先决定往哪段链路继续收窄

线程池这层已经亮红灯时,继续盯参数表通常收效不大。我更常用的分流方法,是先判断线程到底被什么拖住,再决定下一篇该切到哪里:

我想保住的其实只有一个判断顺序:先确认队列是不是持续堆积,再看任务本身有没有变慢,接着查下游、数据库、CPU,最后才回头看 GC 和参数。线程池容易被误当成起点,但大多数时候它只是把上游问题放大得更明显了。

如果这个顺序不乱,线程池问题通常不会查得太偏。

线程池打满以后,先查队列,再查慢任务,再查下游阻塞、数据库、CPU,最后再看 GC。

把这句留在值班脑子里,比单独背几组参数更有用。