死锁是什么?如何避免死锁?
死锁是什么,以及在并发程序中如何避免死锁一直是面试官偏爱的一个问题。
本文尽量以最简洁的示例来帮助你快速理解,掌握死锁发生的原因及其解决方法。在阅读接下来的内容之前,你必须具备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
....
...
..
.
无限阻塞下去
如何避免死锁?
教科书般的回答应该是,结合“哲学家就餐[1]”模型,分析并总结出以下死锁的原因,最后得出“避免死锁就是破坏造成死锁的,若干条件中的任意一个”的结论。
造成死锁必须达成的4个条件(原因):
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
但是,“哲学家就餐”光看名字就很讨厌,然后以上这4个条件看起来也很绕口,再加上笔者又是个懒人,所以要让我在面试时把这些“背诵”出来实在是太难了!必须要想办法把这4个条件简化一下!
于是,通过对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顺利执行完毕。
总结
是否能够简单明了的在面试中阐述清楚死锁产生的原因,并给出解决死锁的方案,可以体现程序员在面对对并发问题时思路是否清晰,对并发的基础掌握是否牢固等等。
而且在实际项目中并发模块的逻辑往往比本文的示例复杂许多,所以写并发应用之前一定要充分理解本文所总结的要点,并切记,并发程序编程在不显著影响程序性能的情况下,一定要尽可能的保守。