探秘死锁:原理、发生条件及解决方案
死锁是多线程编程中常见的一个问题,它会导致程序停止响应,进而影响系统的稳定性和性能。理解死锁的原理、发生条件以及如何预防和解决死锁是编写健壮并发程序的关键。
1. 死锁的定义
死锁是指两个或多个线程在执行过程中因争夺资源而相互等待,从而使得这几个线程都无法继续执行。死锁会导致系统资源无法被正常利用,进而影响系统的稳定性。
如上图所示死锁状态,线程 A 己经持有了资源 2,它同时还想申请资源 1,可是此时线程 B 已经持有了资源 1 ,线程 A 只能等待。
反观线程 B 持有了资源 1 ,它同时还想申请资源 2,但是资源 2 已经被线程 A 持有,线程 B 只能等待。所以线程 A 和线程 B 就因为相互等待对方已经持有的资源,而进入了死锁状态。
2. 死锁的四个必要条件
根据Coffman提出的经典死锁四个必要条件(又称Coffman条件):
- 互斥(Mutual Exclusion):资源不能被多个线程同时使用。
- 持有并等待(Hold and Wait):一个线程已经持有了至少一个资源,同时又在等待获取额外的资源。
- 不可剥夺(No Preemption):线程获得的资源在使用完之前不能被强行剥夺,只能由线程自己释放。
- 循环等待(Circular Wait):存在一个循环等待链,即每个线程都在等待下一个线程所持有的资源。
3. 死锁的示例代码
以下是一个Java示例,展示了死锁的产生:
public class DeadlockExample {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: locked resource 1");
try { Thread.sleep(50); } catch (InterruptedException e) {}
synchronized (resource2) {
System.out.println("Thread 1: locked resource 2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: locked resource 2");
try { Thread.sleep(50); } catch (InterruptedException e) {}
synchronized (resource1) {
System.out.println("Thread 2: locked resource 1");
}
}
});
thread1.start();
thread2.start();
}
}
在这个示例中,thread1首先锁住resource1,然后试图锁住resource2。与此同时,thread2首先锁住resource2,然后试图锁住resource1。这会导致两个线程相互等待,从而产生死锁。
4. 死锁的预防和避免
破坏互斥条件
- 使资源尽量支持共享访问,例如使用读写锁来允许多个线程同时读取资源。
破坏持有并等待条件
- 线程在开始时一次性请求所需要的所有资源,如果没有得到全部资源,则释放已获得的资源并重新请求。
- 使用锁定机制时,可以尝试锁定时限,如果超时则释放已持有的锁。
破坏不可剥夺条件
- 设计资源请求策略,使得可以强制抢占资源。例如,使用可重入锁(ReentrantLock)和条件变量(Condition)来支持资源的强制释放。
破坏循环等待条件
- 为资源分配一个全局顺序,线程按照固定顺序请求资源,避免形成循环等待链。
5. 死锁检测与恢复
死锁检测
- 系统可以定期检测资源分配图,判断是否存在循环等待。如果发现死锁,则需要进行相应的恢复操作。
死锁恢复
- 资源抢占:强制从某个线程中剥夺资源,分配给其他需要资源的线程。
- 回滚:回滚部分或全部死锁进程的操作,使其释放所持有的资源。
- 终止进程:直接终止部分或全部死锁进程,从而释放资源。
示例代码中的解决方案
以下是改进后的代码,避免了死锁:
public class DeadlockFreeExample {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: locked resource 1");
try { Thread.sleep(50); } catch (InterruptedException e) {}
synchronized (resource2) {
System.out.println("Thread 1: locked resource 2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource1) { // 改变锁顺序
System.out.println("Thread 2: locked resource 1");
try { Thread.sleep(50); } catch (InterruptedException e) {}
synchronized (resource2) {
System.out.println("Thread 2: locked resource 2");
}
}
});
thread1.start();
thread2.start();
}
}
在这个改进后的例子中,我们确保了两个线程都以相同的顺序(先锁resource1,再锁resource2)来获取锁,从而避免了循环等待的条件。
结论
理解死锁的原理及其四个必要条件是预防和解决死锁问题的基础。通过合理的设计和编程技巧,可以有效地避免死锁,提高并发程序的健壮性和性能。