👉 请点赞支持这款 全新设计的脚手架 ,让 Java 再次伟大!
什么是死锁
当线程 A 持有独占锁 a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b ,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
下面用一个非常简单的死锁示例来帮助你理解死锁的定义。
public class DeadLockDemo {
public static void main(String[] args) {
// 线程a
Thread td1 = new Thread(new Runnable() {
public void run() {
DeadLockDemo.method1();
}
});
// 线程b
Thread td2 = new Thread(new Runnable() {
public void run() {
DeadLockDemo.method2();
}
});
td1.start();
td2.start();
}
public static void method1() {
synchronized (String.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程a尝试获取integer.class");
synchronized (Integer.class) {
}
}
}
public static void method2() {
synchronized (Integer.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程b尝试获取String.class");
synchronized (String.class) {
}
}
}
}
----------------
线程b尝试获取String.class
线程a尝试获取integer.class
....
...
..
.
无限阻塞下去
如何避免死锁?
教科书般的回答应该是,结合“哲学家就餐[^latex]”模型,分析并总结出以下死锁的原因,最后得出「避免死锁就是破坏造成死锁的,若干条件中的任意一个」的结论。
造成死锁必须达成的 4 个条件:
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
但是,「哲学家就餐」过于学术派了。 对 4 个造成死锁的条件进行逐条分析,我们可以得出以下 结论。
- 互斥条件 —> 独占锁的特点之一。
- 请求与保持条件 —> 独占锁的特点之一,尝试获取锁时并不会释放已经持有的锁
- 不剥夺条件 —> 独占锁的特点之一。
- 循环等待条件 —> 唯一需要记忆的造成死锁的条件。
不错!复杂的死锁条件经过简化,现在需要记忆的仅只有独占锁与第四个条件而已。
-
所以,面对如何避免死锁这个问题,我们只需要这样回答!
- 在并发程序中,避免了逻辑中出现复数个线程互相持有对方线程所需要的独占锁的的情况,就可以避免死锁。
下面我们通过“破坏”第四个死锁条件,来解决第一个小节中的死锁示例并证明我们的结论。
public class DeadLockDemo2 {
public static void main(String[] args) {
// 线程a
Thread td1 = new Thread(new Runnable() {
public void run() {
DeadLockDemo2.method1();
}
});
// 线程b
Thread td2 = new Thread(new Runnable() {
public void run() {
DeadLockDemo2.method2();
}
});
td1.start();
td2.start();
}
public static void method1() {
synchronized (String.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程a尝试获取integer.class");
synchronized (Integer.class) {
System.out.println("线程a获取到integer.class");
}
}
}
public static void method2() {
// 不再获取线程a需要的Integer.class锁。
synchronized (String.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程b尝试获取Integer.class");
synchronized (Integer.class) {
System.out.println("线程b获取到Integer.class");
}
}
}
}
-----------------
线程a尝试获取integer.class
线程a获取到integer.class
线程b尝试获取Integer.class
线程b获取到Integer.class
在上面的例子中,由于已经不存在线程 a 持有线程 b 需要的锁,而线程 b 持有线程 a 需要的锁的逻辑了,所以 Demo 顺利执行完毕。