死锁(Deadlock)是多线程编程中一个常见的问题,指的是两个或多个线程互相持有对方需要的资源,但又都不释放自己持有的资源,导致所有线程都无法继续执行,陷入无限等待的状态。
死锁产生的四个必要条件(缺一不可):
- 互斥条件 (Mutual Exclusion): 资源在同一时刻只能被一个线程占用。这是资源本身的特性,例如打印机、数据库连接等。
- 请求与保持条件 (Hold and Wait): 线程已经持有至少一个资源,但又提出了新的资源请求,而新资源被其他线程占用,此时请求线程会阻塞,但不会释放自己已持有的资源。
- 不可剥夺条件 (No Preemption): 线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只能由持有资源的线程主动释放。
- 循环等待条件 (Circular Wait): 存在一个线程集合 {T0, T1, …, Tn},其中 T0 等待 T1 持有的资源,T1 等待 T2 持有的资源,…,Tn 等待 T0 持有的资源,形成一个环路。
常见导致死锁的情况:
-
嵌套锁: 线程 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(); } }
-
动态锁顺序: 如果获取锁的顺序不是固定的,而是根据运行时条件决定的,也可能导致死锁。
// 假设 transferMoney 方法在不同的线程中以不同的账户顺序调用 public void transferMoney(Account fromAccount, Account toAccount, int amount) { synchronized (fromAccount) { synchronized (toAccount) { // ... 转账逻辑 ... } } }
-
资源耗尽:如果线程池中所有线程都被阻塞,去等待一个不可能释放的资源,则会造成线程池耗尽,导致死锁。
-
wait/notify使用不当
- 线程在没有获取到锁的情况下调用
wait()
或notify()
/notifyAll()
。 - 线程在
wait()
之后没有被正确地notify()
或notifyAll()
。 - 线程在持有锁的情况下调用了
wait()
,但没有其他线程能够获取锁并调用notify()
。
- 线程在没有获取到锁的情况下调用
避免死锁的方法:
-
避免嵌套锁: 尽量不要在持有锁的情况下再去获取其他锁。如果必须获取多个锁,尽量使用
ReentrantLock
的tryLock()
方法来尝试获取锁,避免无限等待。 -
固定锁顺序: 如果必须获取多个锁,确保所有线程都按照相同的顺序获取锁。例如,可以对锁对象进行排序,然后按照排序后的顺序获取锁。
//使用账户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){ // ... 转账逻辑 ... } } }
-
超时等待: 在获取锁时设置超时时间,如果超时仍未获取到锁,则放弃并释放已持有的锁。可以使用
ReentrantLock
的tryLock(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 } }
-
死锁检测: 使用工具(如 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); } } } }
-
-
破坏不可剥夺条件: 可以通过设置优先级,或使用显示的锁(ReentrantLock)等机制,来实现锁的抢占。
-
避免资源耗尽: 合理配置线程池的大小,使用有界队列,并设置合适的拒绝策略。
-
正确使用
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 并发编程的知识,并了解如何在实际应用中预防和解决死锁问题。