Skip to content

P04 线上发生死锁定位&避免 ​

面试题:什么是线程死锁?线上发生死锁如何快速定位?如何避免死锁?

1. 什么是死锁 ​

一句话:并发场景下,线程因为相互等待对方持有的资源,导致永久阻塞的现象。

经典"转账"例子:

  • 线程 1:张三→李四转账,先锁"张三"账户,再申请"李四"账户的锁;
  • 线程 2:李四→张三转账,先锁"李四"账户,再申请"张三"账户的锁;
  • 结果:各自拿着一把锁、都在等对方手里的锁,互相不让,程序卡死。

2. 线上死锁如何快速定位(实操) ​

bash
jps        # 找到 Java 进程 PID,如 79400
jstack 79400

jstack 输出会直接出现 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;杜绝:破坏互斥/占有且等待/不可抢占/循环等待四条件之一;权衡:了解银行家算法。

基于 VitePress 重建