Skip to content

死锁

两个线程互相等待对方持有的锁,谁也不放手。

一个经典场景

java
public class DeadlockDemo {

    private static final Object resourceA = new Object();
    private static final Object resourceB = new Object();

    public static void main(String[] args) {
        // 线程1:先拿 A,再拿 B
        new Thread(() -> {
            synchronized (resourceA) {
                System.out.println("线程1: 拿到资源A");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (resourceB) {  // 等 B
                    System.out.println("线程1: 拿到资源B");
                }
            }
        }, "Thread-1").start();

        // 线程2:先拿 B,再拿 A
        new Thread(() -> {
            synchronized (resourceB) {
                System.out.println("线程2: 拿到资源B");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (resourceA) {  // 等 A
                    System.out.println("线程2: 拿到资源A");
                }
            }
        }, "Thread-2").start();
    }
}

运行结果:程序卡死。

线程1: 拿到资源A
线程2: 拿到资源B
(卡住不动了)

死锁的四个必要条件

发生死锁必须同时满足以下四个条件:

条件含义
互斥条件资源只能被一个线程持有
持有并等待线程持有资源,同时等待其他资源
不可抢占资源不能被强制释放
循环等待形成 A 等 B、B 等 A 的循环

破坏任一条件,死锁就不会发生。

如何排查死锁?

方法一:jstack 检测

bash
# 找到 Java 进程
jps -l

# 导出线程堆栈
jstack <pid>

输出

Found one Java-level deadlock:
=========================
"Thread-2":
  waiting for monitor lock 0x00007f8a5400a000,
  which is held by "Thread-1"

"Thread-1":
  waiting for monitor lock 0x00007f8a54009000,
  which is held by "Thread-2"

Java stack information for the threads listed above:
==================================================
"Thread-2":
        at DeadlockDemo.lambda$main$1(DeadlockDemo.java:20)
        - waiting to lock <0x00007f8a54009000>
        - locked <0x00007f8a5400a000>

"Thread-1":
        at DeadlockDemo.lambda$main$0(DeadlockDemo.java:10)
        - waiting to lock <0x00007f8a5400a000>
        - locked <0x00007f8a54009000>

jstack 会自动检测并报告死锁。

方法二:JConsole / VisualVM

图形化工具查看线程状态。

方法三:代码检测

java
// 定时检测死锁
ThreadMXBean threadMX = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMX.findDeadlockedThreads();
if (deadlockedThreads != null) {
    for (long id : deadlockedThreads) {
        ThreadInfo info = threadMX.getThreadInfo(id);
        System.out.println("死锁线程: " + info.getThreadName());
    }
}

如何解决死锁?

方案一:固定加锁顺序

破坏循环等待条件

java
public class FixedOrderSolution {

    private static final Object resourceA = new Object();
    private static final Object resourceB = new Object();

    // 始终按固定顺序获取
    public void method1() {
        synchronized (resourceA) {  // 始终先拿 A
            System.out.println("method1: 拿到资源A");
            synchronized (resourceB) {
                System.out.println("method1: 拿到资源B");
            }
        }
    }

    public void method2() {
        synchronized (resourceA) {  // 同样先拿 A
            System.out.println("method2: 拿到资源A");
            synchronized (resourceB) {
                System.out.println("method2: 拿到资源B");
            }
        }
    }
}

注意:按对象的「天然顺序」(如 ID、hashCode)排序更好:

java
public void transfer(Account from, Account to, int amount) {
    // 按 ID 排序,保证顺序一致
    Account first = from.id < to.id ? from : to;
    Account second = from.id < to.id ? to : from;

    synchronized (first) {
        synchronized (second) {
            // 转账
        }
    }
}

方案二:tryLock 超时

破坏不可抢占条件

java
public class TryLockSolution {

    private static final ReentrantLock lockA = new ReentrantLock();
    private static final ReentrantLock lockB = new ReentrantLock();

    public void transfer() {
        while (true) {
            // 尝试获取 lockA,最多等 1 秒
            if (lockA.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    // 尝试获取 lockB,最多等 1 秒
                    if (lockB.tryLock(1, TimeUnit.SECONDS)) {
                        try {
                            // 执行操作
                            System.out.println("转账成功");
                            return;
                        } finally {
                            lockB.unlock();
                        }
                    }
                } finally {
                    lockA.unlock();
                }
            }
            // 都没拿到,休息一下再重试
            Thread.sleep(10);
        }
    }
}

方案三:使用单个锁

破坏持有并等待条件

java
public class SingleLockSolution {

    private final Object globalLock = new Object();
    private int resourceA = 0;
    private int resourceB = 0;

    // 两个操作都在同一个锁里
    public void method1() {
        synchronized (globalLock) {
            resourceA++;
            resourceB--;
        }
    }

    public void method2() {
        synchronized (globalLock) {
            resourceA--;
            resourceB++;
        }
    }
}

缺点:并发性能降低。

方案四:LockTimeout + 回滚

java
public class TimeoutWithRollback {

    private final ReentrantLock lock1 = new ReentrantLock();
    private final ReentrantLock lock2 = new ReentrantLock();

    public boolean transfer(Account from, Account to, int amount) {
        try {
            // 尝试获取两个锁
            if (!lock1.tryLock(1, TimeUnit.SECONDS)) return false;
            if (!lock2.tryLock(1, TimeUnit.SECONDS)) return false;

            // 执行转账
            if (from.balance >= amount) {
                from.balance -= amount;
                to.balance += amount;
                return true;
            }
            return false;

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock2.isHeldByCurrentThread()) lock2.unlock();
            if (lock1.isHeldByCurrentThread()) lock1.unlock();
        }
    }
}

实战:银行转账

java
public class BankTransfer {

    private final Map<String, Account> accounts = new HashMap<>();
    private final ReentrantLock lock = new ReentrantLock();

    public void transfer(String fromId, String toId, int amount) {
        Account from = accounts.get(fromId);
        Account to = accounts.get(toId);

        // 按 ID 排序,保证加锁顺序一致
        Account first = from.id.compareTo(to.id) < 0 ? from : to;
        Account second = from.id.compareTo(to.id) < 0 ? to : from;

        lock.lock();
        try {
            if (first.balance >= amount) {
                first.balance -= amount;
                second.balance += amount;
                System.out.println("转账成功: " + fromId + " -> " + toId + " = " + amount);
            }
        } finally {
            lock.unlock();
        }
    }

    static class Account {
        String id;
        int balance;

        Account(String id, int balance) {
            this.id = id;
            this.balance = balance;
        }
    }
}

死锁 vs 活锁

死锁活锁
线程状态阻塞运行中
现象都不动都在动,但没进展
原因互相等待不断重试

活锁示例

java
// 两个线程都在运行,但都在礼让对方
while (true) {
    if (tryLock(resourceA)) {
        if (tryLock(resourceB)) {
            // 成功
            return;
        }
        unlock(resourceA);
    }
    // 立即重试,导致一直冲突
}

解决活锁:加入随机退避时间。

java
while (true) {
    if (lockA.tryLock()) {
        if (lockB.tryLock()) {
            return;
        }
        lockA.unlock();
    }
    Thread.sleep(new Random().nextInt(100));  // 随机退避
}

总结

死锁的四个条件(同时满足才发生):

  1. 互斥条件
  2. 持有并等待
  3. 不可抢占
  4. 循环等待

解决方案

方案破坏条件适用场景
固定加锁顺序循环等待多锁必须同时获取
tryLock 超时不可抢占复杂锁场景
单锁持有并等待简单场景
随机退避活锁重试冲突

最佳实践

  1. 尽量避免多锁场景
  2. 多锁时用固定顺序
  3. 用 tryLock 防止永久等待
  4. 线上定期检测死锁

基于 VitePress 构建