“死锁”或者Deadlock是计算机科学中一个重要的概念,说得是在并发系统中的一种状态,其中多个进程或线程无限期地等待资源,而无法继续执行下去。当发生死锁时,系统中的进程或线程会陷入一种僵持状态,无法继续进行,导致系统无法正常运行。
死锁发生的条件通常包括以下四个必要条件:
1)互斥条件(Mutual Exclusion):至少有一个资源被标记为独占资源,即一次只能被一个进程或线程使用。
2)占有和等待条件(Hold and Wait):进程或线程已经获得了一些资源,并且还在等待获取其他资源,同时保持对已有资源的占有。
3)不可抢占条件(No Preemption):已经分配给进程或线程的资源不能被强制性地剥夺,只能由占有资源的进程或线程主动释放。
4)循环等待条件(Circular Wait):存在一个进程或线程的资源等待链,使得每个进程或线程都在等待下一个资源的释放。
当这四个条件同时满足时,就可能发生死锁。一旦发生死锁,系统无法自动解除,因为每个进程或线程都在等待其他资源的释放,形成了循环等待。
“哲学家就餐问题”。这个问题描述了五位哲学家围坐在圆桌旁,每位哲学家面前放着一个盘子和一支叉子,哲学家需要交替地使用左右两边的叉子才能进餐。
情景如下:
哲学家A坐在桌子上,需要获取左右两边的叉子才能进餐。
哲学家B坐在桌子上,需要获取左右两边的叉子才能进餐。
哲学家C坐在桌子上,需要获取左右两边的叉子才能进餐。
哲学家D坐在桌子上,需要获取左右两边的叉子才能进餐。
哲学家E坐在桌子上,需要获取左右两边的叉子才能进餐。
假设每位哲学家都首先尝试获取左边的叉子,然后再尝试获取右边的叉子。如果所有哲学家同时开始进餐,并且每个哲学家都先拿起左边的叉子,然后等待右边的叉子,那么这种情况下可能会导致死锁。
具体流程如下:
哲学家A尝试获取左边的叉子,但叉子已被哲学家E占有,所以哲学家A等待左边的叉子。
哲学家B尝试获取左边的叉子,但叉子已被哲学家A占有,所以哲学家B等待左边的叉子。
哲学家C尝试获取左边的叉子,但叉子已被哲学家B占有,所以哲学家C等待左边的叉子。
哲学家D尝试获取左边的叉子,但叉子已被哲学家C占有,所以哲学家D等待左边的叉子。
哲学家E尝试获取左边的叉子,但叉子已被哲学家D占有,所以哲学家E等待左边的叉子。
在这个例子中,每个哲学家都占有一个叉子,并等待另一个叉子的释放,但没有哲学家能够继续执行下去,系统陷入了死锁状态。
假设有两个进程A和B,以及两个共享资源X和Y。每个进程需要同时占有两个资源才能完成任务。
情景如下:
进程A获得了资源X,并等待资源Y。
进程B获得了资源Y,并等待资源X。
此时,两个进程都无法继续执行,因为它们都在等待对方占有的资源。这就是一个死锁的状态,两个进程都无法继续执行下去,系统陷入僵持状态。
具体流程如下:
进程A开始执行,并获得资源X。
进程B开始执行,并获得资源Y。
进程A尝试请求资源Y,但此时资源Y已被进程B占有,所以进程A等待资源Y释放。
进程B尝试请求资源X,但此时资源X已被进程A占有,所以进程B等待资源X释放。
在这个例子中,互斥条件满足,因为资源X和Y只能被一个进程占有。占有和等待条件也满足,因为进程A占有资源X并等待资源Y,进程B占有资源Y并等待资源X。不可抢占条件也满足,因为已经分配给进程的资源不能被强制性地剥夺。最后,循环等待条件满足,因为进程A等待进程B占有的资源,进程B等待进程A占有的资源。
当涉及多个线程并发执行时,线程死锁是一个常见的问题。下面是一个简单的Java线程死锁的例子,涉及两个线程和两个共享资源:
public class DeadlockExample {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
// 线程1
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1 acquired resource 1");
try {
Thread.sleep(100); // 等待一段时间,给线程2机会获取资源
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("Thread 1 acquired resource 2");
}
}
});
// 线程2
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2 acquired resource 2");
synchronized (resource1) {
System.out.println("Thread 2 acquired resource 1");
}
}
});
// 启动线程
thread1.start();
thread2.start();
}
}
在这个例子中,线程1尝试获取资源1,然后等待一段时间,给线程2机会获取资源2。同时,线程2尝试获取资源2,然后等待线程1释放资源1。由于线程1持有资源1并等待资源2,而线程2持有资源2并等待资源1,两个线程陷入了相互等待的状态,造成死锁。
1)预防死锁
2)避免死锁
3)检测与恢复。
预防死锁是通过破坏死锁产生的四个条件之一来避免死锁的发生。避免死锁是在资源分配过程中使用算法和策略,动态地避免可能导致死锁的资源分配组合。检测与恢复是在系统中定期检测死锁的存在,并采取恢复措施,如剥夺资源或通过进程终止来解除死锁。
1.应对“哲学家就餐问题”
资源有序分配:引入一个全局的资源分配策略,确保哲学家按照一定的顺序获取叉子,从而避免循环等待。例如,可以规定哲学家只有在同时获取到左右两个叉子时才能进餐,或者规定奇数号哲学家先拿左边的叉子,偶数号哲学家先拿右边的叉子等。这样可以避免所有哲学家同时竞争同一个叉子,减少死锁的可能性。
资源分配时限:引入一个超时机制,如果一个哲学家等待时间过长仍未获取到所需的叉子,就放弃当前的叉子并重新开始整个获取过程,避免长时间的资源占有。这可以避免一个哲学家长时间等待一个叉子而导致死锁的发生。
可预见性资源分配:通过将资源分配预先安排好,确保每个哲学家在执行过程中能够立即获取到所需的叉子,从而避免竞争和死锁。例如,可以为每个哲学家分配一个专属的叉子,或者为奇数号哲学家分配左边的叉子,偶数号哲学家分配右边的叉子等。
死锁检测与恢复:使用死锁检测算法来监测系统是否陷入死锁状态,一旦检测到死锁,采取相应的措施进行恢复。例如,可以通过中断一个或多个哲学家的进餐过程,释放其占有的叉子,打破死锁的循环等待条件。
2.应多“AB进程抢占XY资源”
资源有序分配:引入一个全局的资源分配策略,确保进程按照一定的顺序获取资源,从而避免循环等待。例如,可以规定进程只有在同时获取到资源X和资源Y时才能执行,或者规定进程A在执行之前必须先获取资源X,进程B在执行之前必须先获取资源Y等。这样可以避免进程之间同时竞争同一个资源,减少死锁的可能性。
资源分配时限:引入一个超时机制,如果一个进程等待时间过长仍未获取到所需的资源,就放弃当前的资源并重新开始整个获取过程,避免长时间的资源占有。这可以避免一个进程长时间等待一个资源而导致死锁的发生。
避免持有多个资源:如果可能,设计系统时尽量避免一个进程同时持有多个资源。如果一个进程只需要其中一个资源,而不需要同时持有两个资源,就能减少死锁的可能性。
死锁检测与恢复:使用死锁检测算法来监测系统是否陷入死锁状态,一旦检测到死锁,采取相应的措施进行恢复。例如,可以通过中断一个或多个进程的执行,释放其占有的资源,打破死锁的循环等待条件。
3.应对Java多线程产生的死锁
Java线程死锁问题,可以采取以下解决办法:
避免嵌套锁定:确保在一个线程持有锁时,不去请求其他的锁。可以通过合理设计代码逻辑和锁的粒度来避免嵌套锁定的情况发生。
锁的顺序获取:约定所有线程获取锁的顺序,按照相同的顺序获取锁可以避免循环等待的情况。例如,对于示例中的资源1和资源2,可以约定所有线程获取资源的顺序是先获取资源1,再获取资源2。
死锁检测与恢复:使用死锁检测算法来监测系统是否陷入死锁状态,一旦检测到死锁,可以通过中断某些线程或回滚操作来恢复系统。Java中的线程管理工具和死锁检测工具可以帮助进行死锁检测和恢复。
例如Java中的Lock接口提供了tryLock()方法,可以尝试获取锁而不进入等待状态。通过使用tryLock()方法,可以在获取锁失败时立即释放已经获取的锁,避免发生死锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockExample {
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
boolean locked1 = false;
boolean locked2 = false;
try {
locked1 = lock1.tryLock(); // 尝试获取锁1
locked2 = lock2.tryLock(); // 尝试获取锁2
} finally {
if (locked1 && locked2) {
System.out.println("Thread 1 acquired both locks");
// 执行相关操作
lock2.unlock();
lock1.unlock();
} else if (locked1) {
lock1.unlock();
} else if (locked2) {
lock2.unlock();
}
}
});
Thread thread2 = new Thread(() -> {
boolean locked1 = false;
boolean locked2 = false;
try {
locked2 = lock2.tryLock(); // 尝试获取锁2
locked1 = lock1.tryLock(); // 尝试获取锁1
} finally {
if (locked1 && locked2) {
System.out.println("Thread 2 acquired both locks");
// 执行相关操作
lock1.unlock();
lock2.unlock();
} else if (locked1) {
lock1.unlock();
} else if (locked2) {
lock2.unlock();
}
}
});
thread1.start();
thread2.start();
}
}
合理的资源管理:在设计多线程应用时,合理管理共享资源是非常重要的。使用并发容器或线程安全的数据结构可以减少对显式锁的需求,从而降低发生死锁的风险。例如下面的例子中使用ConcurrentHashMap:
import java.util.concurrent.ConcurrentHashMap;
public class ResourceManagementExample {
private static final ConcurrentHashMap<Integer, Integer> resources = new ConcurrentHashMap<>();
public static void main(String[] args) {
// 初始化资源
resources.put(1, 1);
resources.put(2, 2);
resources.put(3, 3);
Thread thread1 = new Thread(() -> {
// 获取资源1
Integer resource1 = resources.get(1);
synchronized (resource1) {
System.out.println("Thread 1 acquired resource 1");
try {
Thread.sleep(100); // 模拟执行某些操作
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取资源2
Integer resource2 = resources.get(2);
synchronized (resource2) {
System.out.println("Thread 1 acquired resource 2");
// 执行相关操作
}
}
});
Thread thread2 = new Thread(() -> {
// 获取资源2
Integer resource2 = resources.get(2);
synchronized (resource2) {
System.out.println("Thread 2 acquired resource 2");
// 获取资源3
Integer resource3 = resources.get(3);
synchronized (resource3) {
System.out.println("Thread 2 acquired resource 3");
// 执行相关操作
}
}
});
thread1.start();
thread2.start();
}
}