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

死锁(Deadlock)是多线程编程中常见的问题,指的是两个或多个线程互相持有对方所需的资源,导致所有线程都无法继续执行的情况。死锁的发生需要满足以下四个必要条件(称为死锁的四个条件):


1. 死锁的四个条件

  1. 互斥条件(Mutual Exclusion)
    • 资源一次只能被一个线程占用。
  2. 占有并等待(Hold and Wait)
    • 线程持有至少一个资源,并等待获取其他被占用的资源。
  3. 非抢占条件(No Preemption)
    • 线程持有的资源不能被其他线程强行抢占,只能由线程主动释放。
  4. 循环等待条件(Circular Wait)
    • 存在一个线程等待的循环链,每个线程都在等待下一个线程所持有的资源。

只有当这四个条件同时满足时,死锁才会发生。


2. 死锁的示例

以下是一个典型的死锁示例:

public class DeadlockExample {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            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: Acquired lock 2!");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            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: Acquired lock 1!");
                }
            }
        });

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

在这个示例中:

  • Thread 1 持有 lock1,并等待 lock2
  • Thread 2 持有 lock2,并等待 lock1
  • 两个线程互相等待,导致死锁。

3. 如何避免死锁

为了避免死锁,可以尝试破坏死锁的四个必要条件中的至少一个。以下是常见的避免死锁的策略:

(1)破坏占有并等待条件
  • 一次性申请所有需要的资源,而不是逐个申请。
  • 如果无法一次性获取所有资源,则释放已持有的资源并重试。

示例

synchronized (lock1) {
    synchronized (lock2) {
        // 一次性获取所有锁
    }
}
(2)破坏非抢占条件
  • 允许线程抢占其他线程持有的资源。
  • 使用 Lock 类(如 ReentrantLock)的 tryLock() 方法,尝试获取锁,如果失败则释放已持有的锁。

示例

Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();

if (lock1.tryLock()) {
    try {
        if (lock2.tryLock()) {
            try {
                // 成功获取所有锁
            } finally {
                lock2.unlock();
            }
        }
    } finally {
        lock1.unlock();
    }
}
(3)破坏循环等待条件
  • 对资源进行排序,要求线程按照固定的顺序申请资源。
  • 例如,所有线程必须先申请 lock1,再申请 lock2

示例

public void method1() {
    synchronized (lock1) {
        synchronized (lock2) {
            // 按照固定顺序获取锁
        }
    }
}

public void method2() {
    synchronized (lock1) {
        synchronized (lock2) {
            // 按照固定顺序获取锁
        }
    }
}
(4)使用超时机制
  • 在获取锁时设置超时时间,如果超时则释放已持有的锁并重试。
  • 可以使用 Lock 类的 tryLock(long time, TimeUnit unit) 方法。

示例

if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
    try {
        if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                // 成功获取所有锁
            } finally {
                lock2.unlock();
            }
        }
    } finally {
        lock1.unlock();
    }
}
(5)检测和恢复
  • 使用工具或算法检测死锁,并在检测到死锁时强制释放资源。
  • 例如,Java 的 ThreadMXBean 可以检测死锁。

示例

ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] threadIds = bean.findDeadlockedThreads();
if (threadIds != null) {
    // 处理死锁
}

4. 总结

  • 死锁的发生需要满足四个必要条件:互斥条件、占有并等待、非抢占条件和循环等待条件。
  • 避免死锁的策略包括:
    • 破坏占有并等待条件。
    • 破坏非抢占条件。
    • 破坏循环等待条件。
    • 使用超时机制。
    • 检测和恢复死锁。
  • 在实际开发中,应尽量避免嵌套锁,并按照固定顺序获取锁。

通过合理的设计和编码,可以有效避免死锁问题。

THE END
点赞14 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容