什么是线程死锁?如何排查与解决?
结论先行:死锁(deadlock)是一组线程各自持有对方需要的锁,又都在等对方释放,导致全部永久阻塞。它必须同时满足四个条件:互斥、持有并等待、不可剥夺、循环等待——破坏任意一个就能避免。
1. 四个必要条件
| 条件 | 含义 | 破坏手段 |
|---|---|---|
| 互斥 | 资源同一时刻只能被一个线程持有 | 用无锁结构、只读副本、ThreadLocal |
| 持有并等待 | 拿着 A 的同时去申请 B | 一次性申请所有资源,或申请失败就释放已持有的 |
| 不可剥夺 | 别人持有的锁不能强抢 | tryLock(timeout) 超时后放弃并释放 |
| 循环等待 | 形成 A→B→A 的等待环 | 统一加锁顺序(按 id/哈希排序) |
2. 死锁长什么样
Thread-1: 持有 lockA ──等待──▶ lockB
▲ │
│ ▼
Thread-2: 持有 lockB ──等待──▶ lockA
(互相等对方手里的锁,环闭死)
public class DeadlockDemo {
private static final Object A = new Object();
private static final Object B = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (A) {
sleep(100); // 给另一个线程抢 B 的机会
synchronized (B) { System.out.println("t1"); }
}
}, "Thread-1").start();
new Thread(() -> {
synchronized (B) {
sleep(100);
synchronized (A) { System.out.println("t2"); }
}
}, "Thread-2").start();
}
}
3. 排查三步
第一步:确认是不是死锁。死锁的典型特征是 CPU 不高、线程数稳定、接口全部超时——因为线程都在 BLOCKED 状态睡觉,不消耗 CPU。
# 1. 找到 Java 进程
jps -l
# 2. 导出线程栈,jstack 会自动检测死锁
jstack -l 12345 > /tmp/stack.txt
# 3. 直接搜关键字
grep -A 20 "Found one Java-level deadlock" /tmp/stack.txt
输出大致是:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f8c4c0b2e00 (object 0x000000076ab622a8, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x00007f8c4c0b2e40 (object 0x000000076ab622b8, a java.lang.Object),
which is held by "Thread-1"
"Thread-2":
at com.demo.DeadlockDemo.lambda$main$1(DeadlockDemo.java:19)
- waiting to lock <0x000000076ab622a8> (a java.lang.Object)
- locked <0x000000076ab622b8> (a java.lang.Object)
waiting to lock 加 locked 两行合起来就是"拿着 X 等 Y",环一目了然。
第二步:用代码在线检测。JDK 自带 ThreadMXBean,可以做成定时巡检:
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlocked = bean.findDeadlockedThreads(); // 内部做环检测
if (deadlocked != null) {
for (ThreadInfo info : bean.getThreadInfo(deadlocked, true, true)) {
log.error("死锁线程: {}\n{}", info.getThreadName(), info);
}
}
第三步:定位到业务代码。看两个栈帧共同持有的锁对象,回溯是谁按什么顺序加的锁——通常是一个方法按 A→B、另一个方法按 B→A。
4. 四种修复手段
// 1. 统一加锁顺序:所有地方都按 id 从小到大加锁
public void transfer(Account from, Account to, long amount) {
Account first = from.id < to.id ? from : to;
Account second = from.id < to.id ? to : from;
synchronized (first) {
synchronized (second) {
from.balance -= amount;
to.balance += amount;
}
}
}
// 2. tryLock + 超时:拿不到就放弃并回滚,避免无限等待
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try { doWork(); } finally { lockB.unlock(); }
} else {
log.warn("获取 lockB 超时,回滚");
}
} finally { lockA.unlock(); }
}
// 3. 把两把锁合并成一把(减少锁数量),或用单一锁保护整块状态
// 4. 用无锁结构:AtomicReference + CAS 整体替换状态对象
5. 常见追问
追问一:死锁、活锁、饥饿的区别?
死锁是"都停住不动";活锁(livelock)是"都在动但谁也推进不了"(比如两个线程互相礼让重试);饥饿是"有人一直抢不到"(非公平锁下的长尾线程)。三者排查手段不同:死锁看 jstack,活锁看 CPU 高但业务无进展,饥饿看等待时间分布。
追问二:数据库死锁和 JVM 死锁是一回事吗? 机制相同(都是循环等待),但处理方式不同:数据库(InnoDB)有死锁检测,会主动回滚代价小的事务;JVM 不会自动解开死锁,只能重启或靠代码避免。所以数据库死锁要"重试事务",JVM 死锁只能"预防"。
追问三:为什么线上死锁往往在重启后才暴露? 因为死锁需要特定的交错时序,平时流量下两个线程恰好错开就不会触发。大促、批量任务、定时任务并发时才会撞上——这类问题要靠统一加锁顺序从设计上消灭,而不是靠测试覆盖。
小结
排查死锁记住"CPU 不高但全卡住 → jstack 找 Found one Java-level deadlock → 看 waiting to lock / locked 配对 → 回溯加锁顺序"。解决死锁只有两条正路:统一顺序,或者超时放弃。