Skip to content

死锁是什么?

约 939 字大约 3 分钟

多线程与并发京东

2025-03-25

⭐ 题目日期:

京东 - 2024/12/26

📝 题解:


死锁(Deadlock) 是多线程编程中一种资源竞争引发的僵局,指两个或多个线程在运行过程中因争夺资源而陷入无限等待的状态,导致程序无法继续执行。死锁是并发编程中需要重点防范的严重问题。


死锁产生的必要条件

要形成死锁,必须同时满足以下四个条件:

  1. 互斥条件(Mutual Exclusion)

    • 资源一次只能被一个线程占用(如锁、文件句柄等)。
  2. 持有并等待(Hold and Wait)

    • 线程已持有至少一个资源,同时又在等待其他线程持有的资源。
  3. 不可抢占(No Preemption)

    • 资源只能由持有它的线程主动释放,不能被其他线程强制抢占。
  4. 循环等待(Circular Wait)

    • 存在一组线程,每个线程都在等待下一个线程所持有的资源,形成环形依赖链。

经典死锁示例(Java代码)

public class DeadlockExample {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("Thread1 持有 lockA");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockB) {  // 等待 lockB
                    System.out.println("Thread1 获得 lockB");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("Thread2 持有 lockB");
                synchronized (lockA) {  // 等待 lockA
                    System.out.println("Thread2 获得 lockA");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

输出

Thread1 持有 lockA
Thread2 持有 lockB
(程序卡住,无法继续执行)

如何避免死锁

破坏死锁的四个必要条件中的至少一个即可避免死锁:

1. 破坏循环等待

  • 统一资源申请顺序:所有线程按固定顺序获取资源。
    修改上述示例,让 thread1thread2 按相同顺序获取锁:
    // thread2 修改为:
    synchronized (lockA) {  // 先获取 lockA
        System.out.println("Thread2 持有 lockA");
        synchronized (lockB) { 
            System.out.println("Thread2 获得 lockB");
        }
    }

2. 破坏持有并等待

  • 原子性获取所有资源:一次性申请全部所需资源,若无法获取则释放已持有的资源。
    示例:使用 ReentrantLocktryLock() 方法:
    Lock lockA = new ReentrantLock();
    Lock lockB = new ReentrantLock();
    
    public void doWork() {
        while (true) {
            if (lockA.tryLock()) {
                try {
                    if (lockB.tryLock()) {
                        try {
                            // 执行任务
                            return;
                        } finally {
                            lockB.unlock();
                        }
                    }
                } finally {
                    lockA.unlock();
                }
            }
            // 短暂等待后重试
            try { Thread.sleep(100); } catch (InterruptedException e) {}
        }
    }

3. 允许资源抢占

  • 设置超时机制:尝试获取资源时设定超时时间,超时后释放已有资源并重试。
    示例:使用 Lock.tryLock(long time, TimeUnit unit)
    if (lockA.tryLock(500, TimeUnit.MILLISECONDS)) {
        try {
            if (lockB.tryLock(500, TimeUnit.MILLISECONDS)) {
                try {
                    // 执行任务
                } finally {
                    lockB.unlock();
                }
            }
        } finally {
            lockA.unlock();
        }
    }

4. 减少互斥(非万能)

  • 使用无锁数据结构(如 ConcurrentHashMap)或原子变量(如 AtomicInteger),但仅适用于特定场景。

检测与恢复

  1. 死锁检测

    • 使用工具(如 jstackVisualVM)分析线程转储(Thread Dump),查找阻塞的线程和持有的锁。
    • 代码示例:通过 ThreadMXBean 检测死锁:
      ThreadMXBean bean = ManagementFactory.getThreadMXBean();
      long[] threadIds = bean.findDeadlockedThreads();
      if (threadIds != null) {
          System.out.println("检测到死锁!");
      }
  2. 强制恢复

    • 重启应用或终止部分线程(极端情况,可能导致数据不一致)。

实际开发中的建议

  1. 避免嵌套锁:尽量减少锁的嵌套使用。
  2. 缩小锁范围:只在必要代码块加锁,尽快释放锁。
  3. 使用高级工具:优先选择 java.util.concurrent 包中的并发容器(如 BlockingQueue)和同步器(如 CountDownLatch)。
  4. 代码审查:在团队协作中,重点检查多线程代码的锁顺序和资源依赖。

总结

死锁是并发编程中的“隐形杀手”,需通过合理设计资源管理策略、统一加锁顺序、使用超时机制等方式预防。理解其成因并掌握排查工具,是构建高可靠性多线程应用的必备技能。