P04 线上发生死锁定位&避免
面试题:什么是线程死锁?线上发生死锁如何快速定位?如何避免死锁?
1. 什么是死锁
一句话:并发场景下,线程因为相互等待对方持有的资源,导致永久阻塞的现象。
经典"转账"例子:
- 线程 1:张三→李四转账,先锁"张三"账户,再申请"李四"账户的锁;
- 线程 2:李四→张三转账,先锁"李四"账户,再申请"张三"账户的锁;
- 结果:各自拿着一把锁、都在等对方手里的锁,互相不让,程序卡死。
2. 线上死锁如何快速定位(实操)
bash
jps # 找到 Java 进程 PID,如 79400
jstack 79400jstack 输出会直接出现 Found one Java-level deadlock,并打印出发生死锁的两个线程的完整调用栈,标明:
- 每个线程
locked(锁住了)哪个对象; - 正在
waiting to lock(等待)哪个对象; - 两者交叉持有,形成环。
顺着调用栈就能精确定位到死锁代码行。
补充:线上真发生死锁,临时只能快速重启恢复,否则线程不断堆积,最终会引发 OOM。所以重点在预防。
3. 死锁产生的四个必要条件
必须同时满足,破坏任意一个即可解决:
| 条件 | 含义 | 代码中的体现 |
|---|---|---|
| 互斥 | 资源同一时刻只能被一个线程占用 | synchronized / Lock |
| 占有且等待 | 已占有一个资源,又等待其他线程持有的资源 | 锁 A 后去锁 B |
| 不可抢占 | 资源只能由持有者主动释放 | synchronized 无法被外部释放 |
| 循环等待 | 线程之间形成环形等待链 | 你等我、我等你 |
4. 如何避免死锁(破坏四大条件)
① 破坏互斥:无锁化
不用锁就不会死锁。用 AtomicInteger 原子类、CAS 乐观锁、Semaphore 等代替。
java
AtomicInteger balance = new AtomicInteger(100);
balance.addAndGet(-50);② 破坏占有且等待:一次性申请全部资源
写一个资源管理器(Allocator),用集合记录资源占用,apply() 一次申请全部资源、拿不到就自旋等待,用完 free() 归还:
java
class Allocator {
private final List<Object> used = new ArrayList<>();
public synchronized boolean apply(Object from, Object to) {
if (used.contains(from) || used.contains(to)) return false;
used.add(from); used.add(to);
return true;
}
public synchronized void free(Object from, Object to) {
used.remove(from); used.remove(to);
}
}③ 破坏不可抢占:锁超时
用 Lock 替代 synchronized,拿不到锁就超时放弃,不死等:
java
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try { /* 业务 */ } finally { lock.unlock(); }
} else {
// 超时:记录日志、重试或降级
}④ 破坏循环等待:按固定顺序加锁
所有线程统一按同一顺序加锁(如按账户 ID 升序),就不会形成环:
java
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized (first) {
synchronized (second) { /* 转账 */ }
}5. 高频追问
- 破坏死锁条件有代价吗? 有。除无锁化外,其余方案本质都是把并发变成串行,降低吞吐量,实际中用于"预防"而非"100% 杜绝"。
- 银行家算法是干什么的? 在分配资源前做安全性检查,以一定概率避免死锁,同时兼顾系统吞吐量/性能。
一句话总结
定位:jps + jstack;杜绝:破坏互斥/占有且等待/不可抢占/循环等待四条件之一;权衡:了解银行家算法。