线程池打满以后,应该先查队列、拒绝策略还是慢任务?
线程池打满只是执行层红灯,不等于根因就在参数。把队列堆积、慢任务、下游等待、数据库链路和 CPU / GC 放在同一条排查链里看,才更容易找到真正拖慢 worker 的地方。
线程池打满不是一个配置名词,而是一种执行层拥塞信号。监控里看到 active 顶满、queue 往上爬、reject 开始冒出来时,大家很容易立刻去调 maximumPoolSize、换拒绝策略,或者把队列再垫高一点。
这些动作有时能争取时间,但更常见的情况是:线程不是不够,而是被慢任务、下游等待、数据库链路、CPU 热点或 Full GC 抖动占住了。
所以这篇文章不重复解释 corePoolSize、maximumPoolSize 和拒绝策略的基础概念,而是直接回答一个更像线上排障的问题:
线程池打满以后,先看哪里最容易接近根因,怎样避免一上来就改参数、扩线程、清队列这种高概率误判?
排查时更实用的顺序是:先确认队列是不是持续堆积,再判断是不是慢任务;接着排查下游阻塞、数据库链路、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看起来很大,但实际上根本轮不到生效- 核心线程数偏小,队列先把任务全吞了
这时更该确认的不是队列表面上是不是平静,而是:
- 队列是不是把问题藏起来了
- 线程池是不是根本没有把扩线程机制用起来
- 你的排队策略是不是已经在拖慢整个服务
所以第一步不要急着猜根因,先把你看到的“满”分清形态。
三、把排查顺序固定在执行链上
这是这篇文章最核心的部分。
线程池打满以后,我更建议按下面这条顺序查,而不是想到什么查什么:
- 先看队列:到底是在瞬时抖动,还是持续堆积
- 再看慢任务:单个任务是不是执行时间变长了
- 再看下游阻塞:HTTP、RPC、缓存、消息、文件 IO 是否在卡线程
- 再看数据库:慢 SQL、长事务、连接池等待是否在拖任务
- 再看 CPU:线程是不是都在忙计算、死循环、锁竞争
- 最后看 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 频繁怎么办:先判断是不是内存泄漏》。
十、那拒绝策略要不要查?要,但它通常不是第一优先级
题目里提到拒绝策略,是因为很多人在线程池打满时第一反应就是去看它。
这不是完全错,但优先级通常没那么高。
为什么
因为拒绝策略决定的是:
- 当系统已经顶不住时,怎么失败
它回答的不是:
- 系统为什么顶不住
如果你一上来就纠结 AbortPolicy、CallerRunsPolicy、DiscardPolicy,很容易陷入一个错位排查:
- 你在研究“症状最后怎么表现”
- 却还没搞清“问题最早怎么产生”
拒绝策略真正该怎么看
更合理的问题是:
- 当前拒绝是不是刚开始出现,还是已经持续很久
- 拒绝是否把问题暴露得足够清楚
CallerRunsPolicy是否把主请求线程一起拖慢了DiscardPolicy是否让业务悄悄丢任务却不自知
所以拒绝策略更像是 损失控制和失败方式设计 的一部分,而不是根因排查的起点。
十一、这篇文章和隐藏等待 / CPU 高 / 连接池链的边界
如果你已经确认线程池有拥塞信号,但还拿不准下一步该切哪条线,可以直接按下面这组边界分支。
- 这里讨论的是已经看到执行层拥塞信号的现场,比如 active 打满、queue 持续增长、reject 出现;如果你还停留在“接口慢但资源图都正常”,先去看 接口很慢,但 CPU、GC、数据库都正常,隐藏等待点可能在哪?,把等待链真正落点找出来。
- 这篇重点是判断线程池为什么会满,不负责展开“队列不长但仍慢”的伪正常场景;那类现场更适合接到 线程池队列不长但任务还是慢,常见瓶颈在哪里?。
- 如果你的主要现象是异步 backlog、消息积压、补偿任务堆积,这篇只能先帮你确认执行层会不会拖慢,真正的主线要切到 异步任务越堆越多,问题常常不在异步本身。
- 如果你发现请求线程和业务线程一起变差,重点已经变成“入口线程 vs 执行线程谁先堵”,更适合继续到 Tomcat busy threads 很高,和业务线程池 backlog 是一回事吗?。
- 如果线程主要卡在连接获取、事务等待、锁等待,别继续围着线程池现象打转,直接去看 线程池和数据库连接池的容量,为什么要一起做预算? 和数据库连接池相关文章会更合适。
- 如果线程真的在跑热点计算、空转、异常风暴,这里先只做线程池现象层判断,下游根因要交给 Java 服务 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. 线程池参数文章和这篇故障排查文章是什么边界?
传统参数文章更关心 corePoolSize、maxPoolSize 和拒绝策略怎么配;这篇更关心已经看到 active、queue、reject 变差之后,先判断执行层为什么会拥塞。换句话说,本文讨论的是线程池已经报警之后,现场该怎么收敛,不是日常配参手册。
2. 线程池打满以后,应该先看 queue、active,还是先看拒绝策略?
先用 queue 的变化趋势判断是不是已经形成持续积压,再结合 active 判断线程是否长期不释放。拒绝策略当然要看,但它更多是在系统已经扛不住之后定义怎么失败,不是最早的根因判断入口。
3. 线程池打满一定是线程数太小吗?
不一定,而且很多时候不是。更常见的情况是任务变慢、下游阻塞、数据库卡顿、连接池等待或 CPU 热点,导致线程长时间不释放。
4. 线程池打满但数据库指标不高,还要不要继续查数据库链?
要,尤其是获取连接变慢、事务型接口更慢、pending 更早抬升时。数据库没有整体打满,不代表连接池等待、长事务和锁等待没有在拖慢 worker 线程。
5. 线程池打满和“隐藏等待点”最大的区别是什么?
那篇处理的是“资源图不高,但慢点还没落到具体等待链上”;本文处理的是“已经看到 worker 执行层拥塞信号后,按 queue -> 慢任务 -> 下游 -> 数据库 -> CPU -> GC 的顺序继续收敛”。前者先帮你找等待链落点,后者再解释线程池这层为什么已经开始堵。
6. 线程池打满后,什么时候该继续看容量规划而不是继续停在故障排查?
当你已经确认这不是某次慢 SQL、下游抖动、CPU 热点或 GC 放大,而是长期并发预算和资源边界就设计错了,才更适合继续看 线程池和数据库连接池的容量,为什么要一起做预算?。容量治理是治理收口,不替代本文的故障判断顺序。
十五、看到线程池报警后,我一般会先决定往哪段链路继续收窄
线程池这层已经亮红灯时,继续盯参数表通常收效不大。我更常用的分流方法,是先判断线程到底被什么拖住,再决定下一篇该切到哪里:
- 如果线程池还没明显满,但接口已经慢、资源图又不刺眼,先回到 接口很慢,但 CPU、GC、数据库都正常,隐藏等待点可能在哪?
- 如果队列不长、reject 也不多,但任务就是慢,优先看 线程池队列不长但任务还是慢,常见瓶颈在哪里?
- 如果 backlog 主要出现在异步任务、补偿任务、MQ 消费,优先看 异步任务越堆越多,问题常常不在异步本身
- 如果入口请求线程和业务线程池一起变差,优先看 Tomcat busy threads 很高,和业务线程池 backlog 是一回事吗?
- 如果线程主要卡在连接获取、长事务、锁等待,继续看 线程池和数据库连接池的容量,为什么要一起做预算? 以及数据库连接池等待链文章
- 如果线程真的在跑热点计算、空转、异常风暴,继续看 Java 服务 CPU 高怎么排查:一套更稳的线上定位顺序
我想保住的其实只有一个判断顺序:先确认队列是不是持续堆积,再看任务本身有没有变慢,接着查下游、数据库、CPU,最后才回头看 GC 和参数。线程池容易被误当成起点,但大多数时候它只是把上游问题放大得更明显了。
如果这个顺序不乱,线程池问题通常不会查得太偏。
线程池打满以后,先查队列,再查慢任务,再查下游阻塞、数据库、CPU,最后再看 GC。
把这句留在值班脑子里,比单独背几组参数更有用。