Java 中什么情况会导致死锁?如何避免?

死锁(Deadlock)是多线程编程中一个常见的问题,指的是两个或多个线程互相持有对方需要的资源,但又都不释放自己持有的资源,导致所有线程都无法继续执行,陷入无限等待的状态。

死锁产生的四个必要条件(缺一不可):

  1. 互斥条件 (Mutual Exclusion): 资源在同一时刻只能被一个线程占用。这是资源本身的特性,例如打印机、数据库连接等。
  2. 请求与保持条件 (Hold and Wait): 线程已经持有至少一个资源,但又提出了新的资源请求,而新资源被其他线程占用,此时请求线程会阻塞,但不会释放自己已持有的资源。
  3. 不可剥夺条件 (No Preemption): 线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只能由持有资源的线程主动释放。
  4. 循环等待条件 (Circular Wait): 存在一个线程集合 {T0, T1, …, Tn},其中 T0 等待 T1 持有的资源,T1 等待 T2 持有的资源,…,Tn 等待 T0 持有的资源,形成一个环路。

常见导致死锁的情况:

  1. 嵌套锁: 线程 A 持有锁 L1,然后尝试获取锁 L2;同时,线程 B 持有锁 L2,然后尝试获取锁 L1。

    public class NestedLockDeadlock {
        private final Object lock1 = new Object();
        private final Object lock2 = new Object();
    
        public void method1() {
            synchronized (lock1) {
                System.out.println("Thread 1: Holding lock 1...");
                try {
                    Thread.sleep(100); // 增加死锁发生的概率
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 1: Waiting for lock 2...");
                synchronized (lock2) {
                    System.out.println("Thread 1: Holding lock 1 and lock 2...");
                }
            }
        }
    
        public void method2() {
            synchronized (lock2) {
                System.out.println("Thread 2: Holding lock 2...");
                try {
                    Thread.sleep(100); // 增加死锁发生的概率
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 2: Waiting for lock 1...");
                synchronized (lock1) {
                    System.out.println("Thread 2: Holding lock 2 and lock 1...");
                }
            }
        }
    
        public static void main(String[] args) {
            NestedLockDeadlock deadlock = new NestedLockDeadlock();
            Thread thread1 = new Thread(deadlock::method1);
            Thread thread2 = new Thread(deadlock::method2);
            thread1.start();
            thread2.start();
        }
    }
    
  2. 动态锁顺序: 如果获取锁的顺序不是固定的,而是根据运行时条件决定的,也可能导致死锁。

     // 假设 transferMoney 方法在不同的线程中以不同的账户顺序调用
    public void transferMoney(Account fromAccount, Account toAccount, int amount) {
        synchronized (fromAccount) {
            synchronized (toAccount) {
                // ... 转账逻辑 ...
            }
        }
    }
    
    
  3. 资源耗尽:如果线程池中所有线程都被阻塞,去等待一个不可能释放的资源,则会造成线程池耗尽,导致死锁。

  4. wait/notify使用不当

    • 线程在没有获取到锁的情况下调用 wait()notify()/notifyAll()
    • 线程在 wait() 之后没有被正确地 notify()notifyAll()
    • 线程在持有锁的情况下调用了 wait(),但没有其他线程能够获取锁并调用 notify()

避免死锁的方法:

  1. 避免嵌套锁: 尽量不要在持有锁的情况下再去获取其他锁。如果必须获取多个锁,尽量使用 ReentrantLocktryLock() 方法来尝试获取锁,避免无限等待。

  2. 固定锁顺序: 如果必须获取多个锁,确保所有线程都按照相同的顺序获取锁。例如,可以对锁对象进行排序,然后按照排序后的顺序获取锁。

    //使用账户ID来排序锁
    public void transferMoney(Account fromAccount, Account toAccount, int amount) {
        Account first = fromAccount.getId() < toAccount.getId() ? fromAccount : toAccount;
        Account second = fromAccount.getId() < toAccount.getId() ? toAccount : fromAccount;
        synchronized (first){
            synchronized(second){
                // ... 转账逻辑 ...
            }
        }
    }
    
  3. 超时等待: 在获取锁时设置超时时间,如果超时仍未获取到锁,则放弃并释放已持有的锁。可以使用 ReentrantLocktryLock(long timeout, TimeUnit unit) 方法。

    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class TimeoutLockDeadlock {
        private final Lock lock1 = new ReentrantLock();
        private final Lock lock2 = new ReentrantLock();
    
        public void method1() throws InterruptedException {
            if (lock1.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    System.out.println("Thread 1: Holding lock 1...");
                    Thread.sleep(100);
                    if (lock2.tryLock(1, TimeUnit.SECONDS)) {
                        try {
                            System.out.println("Thread 1: Holding lock 1 and lock 2...");
                        } finally {
                            lock2.unlock();
                        }
                    } else {
                        System.out.println("Thread 1: Unable to acquire lock 2, releasing lock 1...");
                    }
                } finally {
                    lock1.unlock();
                }
            } else {
                System.out.println("Thread 1: Unable to acquire lock 1, giving up...");
            }
        }
    
        public void method2() throws InterruptedException {
          //类似method1
        }
    }
    
    
  4. 死锁检测: 使用工具(如 JConsole, VisualVM)或代码来检测死锁。

    • 代码检测: 可以通过 ThreadMXBean 来检测死锁。

      import java.lang.management.ManagementFactory;
      import java.lang.management.ThreadInfo;
      import java.lang.management.ThreadMXBean;
      
      public class DeadlockDetection {
          public static void main(String[] args) {
              // ... (创建死锁的代码) ...
      
              ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
              long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
      
              if (deadlockedThreads != null) {
                  System.out.println("Deadlock detected!");
                  ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads, true, true);
                  for (ThreadInfo threadInfo : threadInfos) {
                      System.out.println(threadInfo);
                  }
              }
          }
      }
      
  5. 破坏不可剥夺条件: 可以通过设置优先级,或使用显示的锁(ReentrantLock)等机制,来实现锁的抢占。

  6. 避免资源耗尽: 合理配置线程池的大小,使用有界队列,并设置合适的拒绝策略。

  7. 正确使用 wait()/notify()

    • 始终在 synchronized 块或方法中调用 wait(), notify(), 和 notifyAll()
    • while 循环中检查条件,而不是 if 语句,以防止 虚假唤醒
    • 确保在条件满足时调用 notify()notifyAll()
    • 优先使用 notifyAll(),除非你能确定只有一个线程在等待。

问题分析:

这个问题考察了对死锁的理解,包括死锁产生的必要条件、常见场景以及避免死锁的方法。回答时需要清晰地解释这些概念,并提供一些代码示例来说明问题。

与其他问题的知识点联系:

  • Java 中的 synchronized 是怎么实现的? synchronized 是 Java 中实现互斥锁的主要机制,也是死锁产生的常见原因之一。
  • Java 中 ReentrantLock 的实现原理是什么? ReentrantLock 提供了比 synchronized 更灵活的锁机制,可以用来避免死锁(例如,通过 tryLock())。
  • Java 中的 wait、notify 和 notifyAll 方法有什么作用? 不正确地使用 wait()/notify() 可能会导致死锁。
  • 你了解 Java 线程池的原理吗?/如何合理地设置 Java 线程池的线程数? 不合理的线程池配置可能导致资源耗尽,从而引发死锁。
  • 线程的生命周期在 Java 中是如何定义的? 死锁发生时,线程会处于BLOCKED状态。

理解这些联系可以帮助你更全面地掌握 Java 并发编程的知识,并了解如何在实际应用中预防和解决死锁问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

+720

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值