死锁(Deadlock)是指在多个进程或线程的执行过程中,由于竞争资源或者彼此之间的通信而造成的一种相互等待的现象,导致这些进程或线程无法继续执行。死锁问题通常在并发编程和多进程系统中出现。
要发生死锁,必须同时满足以下四个必要条件:
1. 互斥条件(Mutual Exclusion)
资源在任意时刻只能由一个进程使用。如果另一个进程请求该资源,则该进程必须等待,直到资源被释放。
- 例子:一个打印机只能由一个进程在某一时刻使用。如果进程A正在使用打印机,进程B就必须等待。
2. 持有并等待条件(Hold and Wait)
一个进程已经持有至少一个资源,并且还在等待其他资源,而这些资源正被其他进程持有。
- 例子:进程A持有资源X,并请求资源Y;同时,进程B持有资源Y,并请求资源X。两个进程相互等待对方释放资源。
3. 不剥夺条件(No Preemption)
资源不能被强行剥夺,只有持有该资源的进程自己主动释放,其他进程才能获得。
- 例子:如果一个进程持有资源X,则该资源只能由进程主动释放,系统不能强制剥夺该资源。
4. 环路等待条件(Circular Wait)
在系统中存在一个进程等待环,即进程集合 { P 1 , P 2 , . . . , P n } \{P_1, P_2, ..., P_n\} {P1,P2,...,Pn},其中 P 1 P_1 P1 等待 P 2 P_2 P2 持有的资源, P 2 P_2 P2 等待 P 3 P_3 P3 持有的资源,……, P n P_n Pn 等待 P 1 P_1 P1 持有的资源,从而形成一个环路。
- 例子:进程A等待进程B持有的资源,进程B等待进程C持有的资源,而进程C又在等待进程A持有的资源,形成一个循环等待链。
总结
死锁的发生必须同时满足互斥、持有并等待、不剥夺和环路等待这四个条件。要避免死锁,可以通过破坏其中一个或多个条件来实现,例如:
- 允许资源共享以破坏互斥条件。
- 在申请资源时不持有其他资源,破坏持有并等待条件。
- 允许资源被抢占,破坏不剥夺条件。
- 通过资源排序避免循环等待,破坏环路等待条件。
理解这四个条件有助于分析和预防系统中的死锁问题。
记住死锁四个条件的方法可以使用一个简单的谐音口诀:互持不环。
谐音口诀解释:
- 互:互斥条件——资源只能被一个进程占用。
- 持:持有并等待条件——一个进程持有资源,还在等待其他资源。
- 不:不剥夺条件——资源不能被强行剥夺,只能由进程自己释放。
- 环:环路等待条件——多个进程形成循环等待。
快速记忆:
- 互持不环,四条件,一起满足,死锁现。
这句话不仅押韵,而且覆盖了每个条件的关键点。通过不断重复这个口诀,可以快速记住导致死锁的四个必要条件。
下面是一个简单的 Java 程序示例,展示了如何产生死锁。这段代码通过两个线程分别获取两个锁,并尝试按不同的顺序获取对方持有的锁,从而导致死锁。
死锁示例程序
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(new Runnable() {
@Override
public void run() {
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 & 2...");
}
}
}
});
// 启动第二个线程
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
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 & 1...");
}
}
}
});
// 启动线程
thread1.start();
thread2.start();
}
}
程序解释
-
锁对象:程序中定义了两个锁对象
lock1
和lock2
。每个线程在执行过程中需要获取这两个锁。 -
线程 1:首先获取
lock1
,然后尝试获取lock2
。在等待获取lock2
时,线程会暂时挂起。 -
线程 2:首先获取
lock2
,然后尝试获取lock1
。在等待获取lock1
时,线程也会暂时挂起。
死锁的发生
- 线程 1 已经持有
lock1
,但在尝试获取lock2
时被阻塞,因为lock2
正被 线程 2 持有。 - 同时,线程 2 已经持有
lock2
,但在尝试获取lock1
时被阻塞,因为lock1
正被 线程 1 持有。
这就形成了一个典型的死锁:两个线程都在等待对方释放锁,但由于双方都无法释放自己已经持有的锁,程序将进入死循环状态,无法继续执行。
解决死锁的方法
- 避免锁的嵌套:尽量减少嵌套锁的使用。
- 按相同顺序获取锁:确保所有线程都以相同的顺序获取锁,从而避免环路依赖。
- 超时机制:使用带超时的锁获取机制,如
tryLock
,以防止长时间等待导致死锁。