环境:CentOS 7 / JDK 17 / 4 核 8G 演示程序:jvm-tuning-demo(SlowResponseDemo deadlock 场景:两线程交叉持锁) 系列:CPU 100% / 锁竞争 / 频繁 Young GC / 切换风暴 / 下游慢 / 池耗尽 关键词:Found one Java-level deadlock、循环等待、持锁顺序、tryLock
一、事故现场:不是慢,是"死"
某功能完全无响应——不是延迟几秒,是请求挂到天荒地老。CPU 反而很低(线程全卡死了,没人干活)。
死锁 vs 锁竞争的体感区别:
| 现象 | 慢,但请求最终能完成 | 彻底卡死,永不返回 |
| 本质 | 排队,总会轮到 | 循环等待,永远等不到 |
| 可恢复性 | 流量降了自己缓过来 | 只能重启 |
二、实锤:jstack 拉到最后
死锁是所有 JVM 问题里唯一不用自己分析的——jstack 会自动检测并在报告末尾给出"尸检报告":
jstack <PID> | tail -30
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x… (object 0x000000008d71ce98, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x… (object 0x000000008d71ce88, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
"Thread-1":
at …DeadLock.lambda$main$0(SlowResponseDemo.java:150)
– waiting to lock <0x…ce98>
– locked <0x…ce88> ← 拿着 ce88,等 ce98
"Thread-2":
at …DeadLock.lambda$main$1(SlowResponseDemo.java:163)
– waiting to lock <0x…ce88>
– locked <0x…ce98> ← 拿着 ce98,等 ce88
Found 1 deadlock.
读法:每个线程两行——- locked 是它手里攥着的,- waiting to lock 是它伸手要的。两个线程互相伸手要对方攥着的,环就闭合了。
三、死锁四条件(面试必背,破坏任一即解)
对应代码:
// 线程1:lock1 → lock2 // 线程2:lock2 → lock1(顺序相反 = 找死)
synchronized (lock1) { synchronized (lock2) {
synchronized (lock2) {...} synchronized (lock1) {...}
} }
四、调整方案
| 统一全局持锁顺序 | 循环等待 | 所有线程都按 lock1→lock2 顺序申请,环无法形成。首选,成本最低 |
| tryLock(timeout) 超时放弃 | 持有并等待 | 拿不到就松手重试(ReentrantLock),synchronized 做不到这点 |
| 消除嵌套锁 | 持有并等待 | 能一把锁解决就别套两层;能无锁(CAS)就别上锁 |
| 死锁监控 | — | 运维侧用 jstack 定时巡检 + ThreadMXBean.findDeadlockedThreads() 做告警埋点 |
五、排查命令(最简单的一篇)
jstack <PID> | tail -30 # JVM 自动汇总 deadlock 报告(jstack -l 效果相同)
jcmd <PID> Thread.print | tail -30 # 等价替代
面试话术:“死锁不用分析,jstack 拉到最后 JVM 自己报告——哪个线程拿哪把锁、等哪把锁、卡在哪一行,全写着。我们要做的是修持锁顺序,让等待环无法闭合。”
口诀:卡死拉到最后看,locked 对 waiting 连成环;统一顺序加超时,环形等待不再现。






