主题
Java 服务出现线程池耗尽、死锁或 CPU 飙高时怎么排查?
题型:问题排查 · 用多次线程快照和运行指标区分忙、等、堵和循环。
排查顺序
- 确认症状:关联业务 P99、超时率、CPU、Load、GC、线程数、线程池活跃数、队列长度和拒绝数,确认是单实例、单接口还是全局问题,并对照流量与发布变更。
- 线上止损:限流、熔断慢下游、隔离异常任务池或摘除故障实例。对结果未知的写请求不能盲目重试,要依赖幂等键、状态查询或对账确认副作用。
- 采集连续现场:间隔数秒获取多份 Thread Dump,必要时结合 JFR、async-profiler 和操作系统线程 CPU。单份快照只能说明瞬时状态,多份才能识别持续运行、持续阻塞或周期性竞争。
- 按线程状态定位:
BLOCKED重点看锁持有者和竞争链;WAITING/TIMED_WAITING结合队列、连接池和 Future 依赖判断是否正常;高 CPU 线程要把系统线程 ID 映射到 Java 栈,检查忙循环、重试风暴、正则回溯、序列化或频繁安全点。 - 检查资源依赖图:确认是否在线程池任务中同步等待同一个池里的子任务,是否持锁调用远程服务,是否存在锁顺序反转,以及连接池容量小于并发需求导致线程全部等待。
- 修复与验证:拆分线程池和 SLA,缩小锁范围,统一锁顺序,消除池内自等待,补充超时、取消和有界队列;通过故障注入和长稳压测验证队列可恢复、无任务丢失且尾延迟受控。
- 复盘监控:增加队列等待时间、最老任务年龄、拒绝数、锁等待、死锁检测、线程数和下游连接池告警,并保留可低成本触发的诊断采样。
可重试性判断
- 还未执行的排队任务通常可以安全取消,但是否重试取决于业务幂等性和剩余时限。
- 已发出远程写请求但本地超时属于结果未知,应先查询状态或对账。
- 参数错误、权限错误等不可重试失败不应进入线程池重试风暴。
易错点
- 线程多不等于死锁,
WAITING也不一定异常;必须结合等待对象、持续时间和业务指标。 - CPU 高不一定是业务线程,也可能是 GC、JIT、本地库或内核开销。
- 只重启能止损但会丢失现场,且不能解决无界队列、锁顺序或下游无超时等根因。