Appearance
deadlock-and-thread-dump
1. 实验目标与工程要点
本实验构造一个确定性双锁死锁:两个线程以相反顺序获取两把锁,必然互相等待对方已持有的锁,从而永久阻塞。目标是让你亲手制造死锁,并学会用线程 dump 定位它——这是线上「卡死类」问题的标准排查手段。
死锁形成的必要条件(四个同时满足才成立)
| 条件 | 含义 | 本实验如何满足 |
|---|---|---|
| 互斥 | 资源同一时刻只能被一个线程持有 | synchronized 锁天然互斥 |
| 持有并等待 | 持有一把锁的同时等待另一把 | T1 持 A 等 B;T2 持 B 等 A |
| 不可剥夺 | 锁只能由持有者主动释放 | synchronized 不会强制剥夺 |
| 循环等待 | 线程间形成等待环路 | T1→等 B(被 T2 持)→ T2→等 A(被 T1 持) |
重点观察
jstack输出里的BLOCKED状态与"Found one Java-level deadlock"自检信息。- 锁拥有者链路:T1 owns
lockAwaiting forlockB;T2 ownslockBwaiting forlockA。
2. 深入原理与生活浅显比喻
2.1 交叉路口比喻
两辆车(T1、T2)分别从左、从右驶入一个单车道窄桥(两把锁 = 桥两段):
- T1 占了桥左段(
lockA),想往前开进桥右段(lockB); - T2 占了桥右段(
lockB),想退回到桥左段(lockA); - 谁都不肯倒车(锁不可剥夺),于是两辆车在桥中央僵死。
这对应死锁四条件的「循环等待」。要打破它,最简单的是统一加锁顺序:大家都先申请左段再申请右段,环路就不成立了。
2.2 为什么 sleep(500) 能让死锁「稳定复现」
源码里每个线程拿到第一把锁后 sleep(500),目的不是制造死锁(顺序相反本身就够),而是大幅提高两个线程都先拿到第一把锁的概率——否则随机时序下可能其中一个线程连续拿到两把锁就跑掉了,死锁看不到。教学 demo 要可复现,所以人为拉开时间窗。
3. 动手步骤
3.1 运行死锁程序
bash
cd labs/java/runtime-concurrency/deadlock-and-thread-dump
javac -d out src/DeadlockDemo.java
java -cp out deadlock.DeadlockDemo &程序会卡住(不会打印「正常结束」),此时另开一个终端:
3.2 抓取线程 dump
bash
# 方式一:jstack(需 PID)
jps # 找到 DeadlockDemo 的 PID
jstack <PID>
# 方式二:向进程发 QUIT 信号(类 Unix)
kill -3 <PID> # 线程 dump 直接打印到标准输出3.3 怎么读 dump(关键片段)
text
Found one Java-level deadlock:
=============================
"Deadlock-T2":
waiting to lock monitor 0x... (object 0x..., a java.lang.Object)
which is held by "Deadlock-T1"
"Deadlock-T1":
waiting to lock monitor 0x... (object 0x..., a java.lang.Object)
which is held by "Deadlock-T2"
Java stack information for the threads listed above:
"Deadlock-T1":
at deadlock.DeadlockDemo.lambda$main$0(DeadlockDemo.java:..)
- waiting to lock <0x...> (a java.lang.Object)
- locked <0x...> (a java.lang.Object) <-- 已持有 lockA
"Deadlock-T2":
at deadlock.DeadlockDemo.lambda$main$1(DeadlockDemo.java:..)
- waiting to lock <0x...> (a java.lang.Object)
- locked <0x...> (a java.lang.Object) <-- 已持有 lockB要点:waiting to lock 与 locked 同现,且两线程的锁互相是对方的 locked,就是死锁的铁证。配合 BLOCKED 状态、Deadlock-T1/Deadlock-T2 的持锁-等待关系,可以一路反推到代码中先拿哪把、后拿哪把。
3.4 如何修复(留作练习)
把 T2 也改成「先 lockA 再 lockB」(与 T1 同序),循环等待消失,死锁解除。这正是模块 08 里「锁顺序固定」原则的来源。
4. 源码
- 死锁构造与运行入口:DeadlockDemo.java
5. 对应知识库文档
- 理论主文档:Java 并发模型总览
- 死锁 / 活锁 / 竞态定位专题:死锁 / 活锁 / 竞态定位