线程耗尽是指所有线程均处于忙碌状态,新任务无法进入、旧任务无法完成,表现为接口变慢、超时频发、队列堆积、拒绝数上升,而CPU和内存未必升高;需通过jstack -l抓取堆栈,重点关注RUNNABLE卡I/O、WAITING集中getTask、BLOCKED争锁三类状态,并实时监控ActiveCount≈maxPoolSize、QueueSize持续增长、RejectedExecutionCount非零等核心指标,结合Arthas追踪任务来源,识别未设超时的HTTP/DB调用、长任务混入短池等问题,同时规避无界队列+小core、过大maxPoolSize+过短keepAliveTime等致命参数组合。

线程耗尽不是“线程没了”,而是“所有线程都在忙,新任务进不来、老任务出不去”。它常表现为接口响应变慢、超时频发、队列持续堆积、拒绝数上涨,但 CPU 和内存未必飙高——表面风平浪静,实则已陷入假死。排查关键在于跳出代码看运行态:线程在哪卡着?谁在占着不放?资源到底被谁吃光了?
看线程堆栈,定位阻塞点
用 jstack -l <pid> 抓取线程快照,重点关注三类状态:
-
RUNNABLE 却不动:比如停在
SocketInputStream.socketRead0或SSLSocketImpl.readApplicationRecord,说明任务卡在网络 I/O 上,极可能是 HTTP/DB 请求没设超时; -
WAITING(on object monitor)过多:尤其集中在
getTask(),说明线程池“吃不饱”——任务提交快、执行慢,线程全在等队列取活干; -
BLOCKED 在锁上:多个线程堵在同一个
synchronized方法或ReentrantLock.lock(),说明瓶颈不在线程池本身,而在任务内部存在串行化竞争。
盯住三个实时指标,判断是否真耗尽
这些数字比配置更真实,必须从 JMX 或 Micrometer 暴露的监控端点读取:
- ActiveCount ≈ maximumPoolSize 且长时间不降 → 线程全被占满,无空闲可用;
- QueueSize 持续增长甚至接近队列容量上限 → 任务压不进线程,全堆在队列里,延迟和 OOM 风险同步上升;
-
RejectedExecutionCount 非零且持续增加 → 拒绝策略已生效,要立刻确认是突发流量、线程池缩容,还是策略本身没日志(如默认
AbortPolicy吞异常)。
查任务来源与执行行为,揪出隐性消耗者
很多线程耗尽并非业务逻辑慢,而是第三方 SDK 或异步回调在“悄悄拖后腿”:
立即学习“Java免费学习笔记(深入)”;
- 用 Arthas 监控
ThreadPoolExecutor.execute(),抓取实际提交任务的调用栈,能快速发现埋在依赖包里的newCachedThreadPool()或嵌套提交逻辑; - 检查任务是否含未关闭的数据库连接、未设 timeout 的 HTTP 调用、未加限流的外部 SDK——一个慢 SQL 或一次超时请求,就能让一个线程挂住几十秒;
- 确认是否有长周期任务混入短时线程池(如定时任务跑报表分析),应拆到独立池,避免污染核心链路。
验证配置陷阱,避免“看着合理,实际致命”
参数组合比单个值更重要:
-
无界队列 + 小 corePoolSize(如
LinkedBlockingQueue()+ core=2)→ 任务全堆内存里,不触发扩容也不拒绝,最终拖垮 JVM; - maximumPoolSize 过大 + keepAliveTime 过短 → 高并发时线程疯涨又快速销毁,上下文切换开销反超收益;
-
拒绝策略无可观测性 → 用
AbortPolicy却没捕获异常打日志,等于把问题“静音”;换成CallerRunsPolicy或自定义策略,至少让调用方感知失败。



















