排查线程耗尽需关注活跃线程数飙高、队列持续积压、任务完成速率下降三指标组合;通过getActiveCount()、getQueue().size()、getTaskCount()-getCompletedTaskCount()等方法交叉验证,并结合jstack或Arthas定位根因。

排查线程耗尽问题,关键不是等系统卡死再救火,而是盯住几个核心指标的异常组合——活跃线程数飙高、队列持续积压、任务完成速率下降,三者同时出现,基本就是线程资源被锁死或打满的明确信号。
看活跃线程数是否顶到上限
调用 getActiveCount() 获取当前正在执行任务的线程数。这个值最能反映真实负载:
- 如果长期等于 getMaximumPoolSize(),说明所有线程都在忙,新任务只能排队或被拒绝
- 若活跃数接近但未达上限,再对比 getCorePoolSize():比如活跃线程 / 核心线程 > 0.9,说明常规容量已严重不足,可能正依赖扩容线程撑住流量
- 注意区分 getPoolSize()(当前总线程数)和 getActiveCount()(真正在干活的线程)——后者才是判断“有没有空闲线程”的直接依据
查队列是否在 silently 积压
调用 getQueue().size() 查看等待任务数,但必须结合队列类型判断:
- 对 LinkedBlockingQueue 或 ArrayBlockingQueue(尤其有界队列),该值有意义;持续增长且远超日常均值(如从平均 5 升至稳定 200+),说明任务进得快、出得慢,线程实际已“假性耗尽”
- 对 SynchronousQueue,size() 恒为 0,此时不能看队列,而要重点检查 拒绝任务数(需自定义 RejectedExecutionHandler 统计)——频繁触发拒绝,本质就是线程池没能力接新活
交叉验证任务处理链是否断裂
单看一个指标容易误判,必须组合分析:
- getTaskCount() - getCompletedTaskCount() ≈ 当前待处理总数(运行中 + 排队中)。如果这个差值持续扩大,而 getActiveCount() 却没同步上升,说明线程“卡住”了——可能在等锁、等IO、或陷入死循环
- getLargestPoolSize() 显著高于当前 getPoolSize(),说明曾扩容过但线程又缩回去了,需查 keepAliveTime 是否过短,导致刚扩容就回收,反复震荡
- isShutdown() 或 isTerminated() 返回 true,但仍有任务提交,会直接导致线程池拒绝一切新任务——这不是耗尽,是提前“停摆”
定位耗尽根源的实操动作
指标异常只是线索,下一步要快速下钻:
- 用 jstack
抓线程堆栈,过滤 RUNNABLE 状态线程,看它们卡在哪个方法(常见于数据库连接、远程调用、锁竞争) - 用 Arthas trace 监控 ThreadPoolExecutor.execute(),确认是谁在高频提交任务(比如某 SDK 的嵌套回调)
- 检查任务本身是否有未捕获异常——线程因异常退出后未被重建,会导致 getPoolSize() 缓慢下降,最终可用线程越来越少
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











