死锁问题与解决方案

#并发 #java #最佳实践

总结
  • 死锁需要同时满足四个条件,破坏任意一个就能避免
  • 最推荐:按固定顺序加锁(破坏循环等待),简单有效
  • 线上推荐用 tryLock + 超时,比 synchronized 更安全
  • 数据库死锁 DB 会自动回滚,但操作行要按固定顺序

1. 先说结论

死锁需要同时满足四个条件,破坏任意一个就能避免。实际开发中最常用的是破坏"循环等待",按固定顺序加锁,简单有效。


2. 四个必要条件

条件 能否破坏 方法
互斥 锁的本质,没法破坏
占用且等待 一次性申请所有资源
不可抢占 申请不到就主动释放已持有的锁
循环等待 按固定顺序申请锁(最推荐)

3. 怎么破坏?

3.1 破坏循环等待(推荐)

按资源 ID 排序,所有线程都按同一顺序加锁,就不会形成环:

void transfer(Account target, int amt) {
    // 按 ID 大小决定加锁顺序,所有线程都一样
    Account first  = this.id < target.id ? this : target;
    Account second = this.id < target.id ? target : this;

    synchronized (first) {
        synchronized (second) {
            if (this.balance > amt) {
                this.balance -= amt;
                target.balance += amt;
            }
        }
    }
}

3.2 破坏不可抢占

tryLock 替代 synchronized,拿不到锁就主动放弃已持有的,随机等一会再重试:

void transfer(Account target, int amt) throws InterruptedException {
    while (true) {
        if (this.lock.tryLock()) {
            try {
                if (target.lock.tryLock()) {
                    try {
                        if (this.balance > amt) {
                            this.balance -= amt;
                            target.balance += amt;
                        }
                        return;
                    } finally {
                        target.lock.unlock();
                    }
                }
            } finally {
                this.lock.unlock(); // 拿不到 target 锁,释放 this 锁
            }
        }
        Thread.sleep((long) (Math.random() * 10)); // 随机退避,避免活锁
    }
}

3.3 破坏占用且等待

用一个 Allocator 统一管理资源,要么一次拿到所有锁,要么一个都不拿:

class Allocator {
    private List<Object> resources = new ArrayList<>();

    synchronized boolean apply(Object from, Object to) {
        if (resources.contains(from) || resources.contains(to)) {
            return false;
        }
        resources.add(from);
        resources.add(to);
        return true;
    }

    synchronized void free(Object from, Object to) {
        resources.remove(from);
        resources.remove(to);
    }
}

void transfer(Account target, int amt) {
    while (!allocator.apply(this, target)) { /* 等待 */ }
    try {
        synchronized (this) {
            synchronized (target) {
                if (this.balance > amt) {
                    this.balance -= amt;
                    target.balance += amt;
                }
            }
        }
    } finally {
        allocator.free(this, target);
    }
}

4. 死锁检测

线上排查用 ThreadMXBean

ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlocked = bean.findDeadlockedThreads();

if (deadlocked != null) {
    for (ThreadInfo info : bean.getThreadInfo(deadlocked)) {
        System.out.println("死锁线程: " + info.getThreadName());
        System.out.println("等待的锁: " + info.getLockName());
    }
}

5. 几个注意点

相关学习路径:原子性解决方案-互斥锁等待-通知机制AQS抽象队列同步器原理