关键在于区分线程“忙”与“卡”,需结合活跃线程数、队列积压、待处理任务总数三指标异常组合,并用jstack或Arthas下钻线程栈定位阻塞点,同时补全任务生命周期观测与异常捕获机制。

排查任务积压,关键不是看线程“有没有在跑”,而是看它们“卡在哪、为什么跑不动”。线程池本身不暴露线程栈或阻塞点,必须结合状态指标与底层线程快照交叉验证。
盯住三个核心指标的异常组合
单个数字容易误判,要同时观察:
- 活跃线程数(getActiveCount)持续高位:比如长期等于最大线程数,说明所有线程都在忙,但未必真在有效执行——可能全卡在数据库查询、远程调用或锁等待上
- 队列积压(getQueue().size)持续上涨:尤其对 LinkedBlockingQueue 或 ArrayBlockingQueue;若使用 SynchronousQueue,该值恒为 0,此时要转而检查拒绝任务数是否突增
- 待处理任务总数(getTaskCount - getCompletedTaskCount)不断变大,但活跃线程数没同步上升:典型“假性空闲”——线程看似在线,实则全部阻塞或已异常退出
区分“忙”和“卡”:用 jstack 或 Arthas 下钻线程栈
getActiveCount 返回的是已进入 run() 方法的线程数,但它不告诉你线程当前在等什么。这时候必须抓真实线程状态:
- 执行
jstack <pid></pid>,过滤出 RUNNABLE 状态的线程,重点看堆栈末尾是否停在:
–java.net.SocketInputStream.read(IO 阻塞)
–java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(显式锁等待)
–com.mysql.cj.jdbc.ConnectionImpl.execSQL(数据库连接未响应) - 用 Arthas 的
thread -n 10查最忙的 10 个线程,或trace java.util.concurrent.ThreadPoolExecutor execute看谁在高频提交任务 - 注意:ForkJoinPool 不适用 jstack 直接分析,需用
vmtool --action getInstances --className java.util.concurrent.ForkJoinWorkerThread
补全任务生命周期观测能力
仅靠快照不够,得知道任务从提交到结束经历了什么:
立即学习“Java免费学习笔记(深入)”;
- 继承 ThreadPoolExecutor,重写
beforeExecute记录任务开始时间、TraceID 和线程名 - 在
afterExecute中统计耗时,并捕获未处理异常(异常会导致 Worker 线程退出,线程数悄悄减少) - 自定义
RejectedExecutionHandler,每次拒绝都打结构化日志,包含任务类型、提交时间、队列 size 和活跃线程数——这是积压爆发的第一现场
识别“空闲却积压”的典型陷阱
以下情况会导致队列涨、活跃低,但系统看起来“没压力”:
- 线程池被意外
shutdown():此时isShutdown()返回 true,新任务直接被拒,但getActiveCount()可能还有几个正在跑的老任务 - 核心线程因异常退出后未重建:比如任务里抛了未捕获的 RuntimeException,Worker 线程终止,但线程池不会自动补新线程(除非配置了 allowCoreThreadTimeOut)
- 所有线程都陷入无限等待:比如 synchronized 块内调用另一个同样持有锁的方法,形成死锁;或使用 CountDownLatch.await() 但没人 countDown()


















