一、什么是死锁?
死锁(Deadlock)是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力干涉,这些线程都将无法推进下去,整个程序会陷入无限期的“假死”状态。
二、死锁产生的四个必要条件
死锁的发生必须同时满足以下四个条件,缺一不可:
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程强行剥夺。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
三、死锁代码示例:经典的“哲学家就餐”问题简化版
我们模拟两位哲学家(线程)和两根筷子(资源)的场景,来完美复现死锁。
public class DeadlockDemo {
// 两根“筷子”,代表两个需要竞争的资源
private static final Object chopstickA = new Object();
private static final Object chopstickB = new Object();
public static void main(String[] args) {
// 哲学家A:先拿A,再拿B
Thread philosopherA = new Thread(() -> {
synchronized (chopstickA) {
System.out.println("哲学家A拿到了筷子A。");
try {
Thread.sleep(100); // 模拟思考,增加死锁概率
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (chopstickB) {
System.out.println("哲学家A拿到了筷子B,开始吃饭。");
}
}
});
// 哲学家B:先拿B,再拿A
Thread philosopherB = new Thread(() -> {
synchronized (chopstickB) {
System.out.println("哲学家B拿到了筷子B。");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (chopstickA) {
System.out.println("哲学家B拿到了筷子A,开始吃饭。");
}
}
});
philosopherA.start();
philosopherB.start();
}
}
运行结果分析:
程序很可能输出以下内容后便永远卡住,不再有任何输出:
哲学家A拿到了筷子A。
哲学家B拿到了筷子B。
死锁过程剖析:
- 线程A先锁定了
chopstickA。 - 几乎同时,线程B锁定了
chopstickB。 - 线程A试图去锁定
chopstickB,但发现它已被线程B持有,于是线程A阻塞,等待B释放。 - 线程B试图去锁定
chopstickA,但发现它已被线程A持有,于是线程B也阻塞,等待A释放。 - 双方都持有对方所需要的资源且不愿释放,同时又都在请求对方已持有的资源,形成了一个循环等待,死锁就此发生。
四、如何避免和预防死锁?
破解死锁的核心思路就是打破四大必要条件中的至少一个。
- 打破循环等待条件(最常用且有效)
-
- 顺序资源申请:强制所有线程以统一的顺序申请资源。在上面的例子中,如果两位哲学家都约定先拿编号小的筷子(比如先拿A再拿B),那么死锁就不会发生。因为线程B会先尝试拿A(被A线程持有而阻塞),等A用完释放A和B后,B才能继续执行。
- 打破请求与保持条件
-
- 一次性申请所有所需资源,在申请不到所有资源时,主动释放已经占有的资源。可以通过
Lock.tryLock()等方法实现。
- 一次性申请所有所需资源,在申请不到所有资源时,主动释放已经占有的资源。可以通过
- 打破不剥夺条件
-
- 允许更高优先级的线程“抢占”低优先级线程已持有的资源。但在Java中
synchronized不支持此特性,使用Lock锁可以实现复杂的尝试或超时获取。
- 允许更高优先级的线程“抢占”低优先级线程已持有的资源。但在Java中
- 使用检测与恢复机制
-
- 这是一种事后补救措施。JDK提供的官方诊断工具,如
jstack,可以自动检测死锁。在命令行运行jstack -l <pid>(pid是Java进程号),工具会清晰地告诉你发现了死锁,并指出哪些线程在等待哪些资源。
- 这是一种事后补救措施。JDK提供的官方诊断工具,如
总结
死锁是并发编程的顽疾,其根源在于对锁的竞争和管理不当。编写代码时,遵循统一的资源申请顺序是预防死锁最简单有效的黄金法则。同时,善用jstack等工具进行问题定位,并考虑使用java.util.concurrent包中更高级的并发工具(如ReentrantLock及其tryLock方法)来替代简单的synchronized,能够为我们提供更灵活、更安全的并发控制手段,从而构建出更加健壮的多线程应用。

被折叠的 条评论
为什么被折叠?



