目录
死锁是指两个或多个线程在互相等待对方释放资源的情况下,无法继续执行的状态。当发生死锁时,线程将永远阻塞,程序也无法正常完成。
导致死锁的原因
死锁是多线程编程中的一种常见问题,它发生在两个或多个线程彼此持有对方所需资源的情况下,导致它们都无法继续执行。这些线程被称为相互等待对方的资源,从而形成了死锁状态。
导致死锁的原因通常可以归结为以下四个必要条件的同时满足:
- 互斥条件(Mutual Exclusion):一个资源每次只能被一个线程占用。当一个线程获得了资源的独占权后,其他线程就无法再访问该资源,直到该线程释放资源。
- 请求与保持条件(Hold and Wait):一个线程在持有了至少一个资源的同时又请求其他资源,而这些资源被其他线程所占用。当这种情况发生时,如果不能立即获得所需的资源,就会阻塞并等待其他线程释放资源。
- 不可剥夺条件(No Preemption):资源只能在线程自愿释放时才能被其他线程占用,其他线程无法强行将其剥夺。即线程需要使用完所有获取到的资源后才会主动释放资源,不会被其他线程打断。
- 循环等待条件(Circular Wait):存在多个线程之间形成一个环形链,每个线程都在等待下一个线程所持有的资源。例如,线程 A 等待线程 B 持有的资源,线程 B 等待线程 C 持有的资源,以此类推,直到线程 N 等待线程 A 持有的资源。
只有当以上四个条件同时满足时,死锁才会发生。解决死锁问题的方法通常包括破坏其中一个或多个必要条件。
避免死锁的方法
避免死锁是多线程编程中重要的问题之一,下面介绍几种常见的避免死锁的方法:
- 加锁顺序:确保所有线程以相同的顺序获得锁。通过强制要求线程按照特定的顺序获取资源锁,可以避免循环等待条件的发生。
- 加锁时限:对获取不到所需资源的线程设置一个超时时间,在超过一定时间后如果仍未获取到资源,则释放已经占用的资源,避免持有并等待条件的发生。这种方法需要谨慎使用,需要根据具体场景来确定超时时间的合理性,并避免引入新的问题。
- 死锁检测:实时监控程序运行状态,检测是否存在死锁。一旦检测到死锁,系统可以采取一些恢复措施,例如强制释放某些资源或者重启线程等。
- 资源分配策略:通过合理的资源分配策略来预防死锁。例如,银行家算法(Banker's Algorithm)用于在分布式系统中避免死锁,通过动态分配资源并根据系统需要进行资源回收。
- 避免持有并等待条件:线程在请求新的资源之前释放已占有的资源。这可以通过设计合适的资源分配算法来实现,例如按照资源请求层级来分配资源,或者采用资源预先分配的方式。
- 使用互斥量和条件变量:互斥量和条件变量是常用的同步机制,在使用时需要合理地获取和释放锁,以遵循加锁顺序,避免死锁的发生。
需要注意的是,以上方法并非适用于所有情况,具体的应用需要根据具体的需求和场景来选择合适的策略。在设计多线程程序时,合理的资源管理和同步机制是避免死锁问题的关键。同时,定期进行代码审查、测试和性能优化也是保证程序健壮性和可靠性的重要手段。
代码示例
以下是同时包含死锁和避免死锁的代码示例,
public class main {
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) { // 获取资源1的锁
System.out.println("线程 1:持有资源 1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 1:等待资源 2");
synchronized (resource2) { // 尝试获取资源2的锁
System.out.println("线程 1:持有资源 1 和资源 2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource1) { // 尝试获取资源1的锁
System.out.println("线程 2:持有资源 1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 2:等待资源 2");
synchronized (resource2) { // 获取资源2的锁
System.out.println("线程 2:持有资源 1 和资源 2");
}
}
});
thread1.start();
thread2.start();
// 等待线程执行完成
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 避免死锁的示例
Object resourceA = new Object();
Object resourceB = new Object();
Thread thread3 = new Thread(() -> {
synchronized (resourceA) { // 获取资源A的锁
System.out.println("线程 3:持有资源 A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 3:等待资源 B");
synchronized (resourceB) { // 尝试获取资源B的锁
System.out.println("线程 3:持有资源 A 和资源 B");
}
}
});
Thread thread4 = new Thread(() -> {
synchronized (resourceA) { // 获取资源A的锁
System.out.println("线程 4:持有资源 A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 4:等待资源 B");
synchronized (resourceB) { // 尝试获取资源B的锁
System.out.println("线程 4:持有资源 A 和资源 B");
}
}
});
thread3.start();
thread4.start();
}
}
在这个例子中,一开始的两个线程 thread1 和 thread2 尝试获取 resource1 和 resource2 的锁以形成死锁。为了避免死锁,我们在这两个线程中修改了获取资源的顺序。其中一个线程先获取 resource1,另一个线程再获取 resource1,这样能够保证资源的获取顺序是一致的,避免了循环等待。
在后面的代码示例中,我们通过尝试获取不同的资源来模拟死锁情况。为了避免死锁,我们采取了避免持有并请求条件的策略,即一个线程只能在释放所有资源之后再请求新的资源。这样能够确保资源的占用和释放是一致的,不会出现死锁的情况。
注意,在上面的示例中,为了能够观察到死锁和避免死锁的效果,我们需要等待线程执行完成。因此,我们在两个线程启动之后使用 join() 方法来等待它们执行完成。