本文参考了以下两篇文章,由衷的感谢两位作者的无私奉献精神。

这篇文章用图例阐释的也很清晰。

死锁的定义

首先来看看死锁的定义:“死锁是指两个或两个以上的进行在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都无法推进下去。”

我们换一个更加规范的定义:“集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该进程就是死锁。”

竞争的资源可以是:锁、网络连接、通知事件,磁盘、带宽,以及一切可以被称作“资源”的东西。

案例演示

当线程 A 持有独占锁 a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,这时进程出现阻塞现象,就是死锁。如下图所示:
接下来用一段代码模拟上述过程:

  1. class DeadLockThread implements Runnable {
  2. //定义 Object 类型的 a 锁对象
  3. static Object a = new Object();
  4. //定义 Object 类型的 b 锁对象
  5. static Object b = new Object();
  6. //定义 boolean 类型的变量 flag
  7. private boolean flag;
  8. //定义有参的构造方法
  9. public DeadLockThread(boolean flag) {
  10. super();
  11. this.flag = flag;
  12. }
  13. @Override
  14. public void run() {
  15. if (flag) {
  16. while (true) {
  17. //a 锁对象上的同步代码块
  18. synchronized (a) {
  19. System.out.println(Thread.currentThread().getName() + "--- if --- a");
  20. //b 锁对象上的同步代码块
  21. synchronized (b) {
  22. System.out.println(Thread.currentThread().getName() + "--- if --- b");
  23. }
  24. }
  25. }
  26. } else {
  27. while (true) {
  28. //b 锁对象上的同步代码块
  29. synchronized (b) {
  30. System.out.println(Thread.currentThread().getName() + "--- else --- b");
  31. //a 锁对象上的同步代码块
  32. synchronized (a) {
  33. System.out.println(Thread.currentThread().getName() + "--- else --- a");
  34. }
  35. }
  36. }
  37. }
  38. }
  39. }
  40. public class example13 {
  41. public static void main(String[] args) {
  42. //创建两个 DeadLockThread 对象
  43. DeadLockThread d1 = new DeadLockThread(true);
  44. DeadLockThread d2 = new DeadLockThread(false);
  45. //创建并开启两个线程
  46. new Thread(d1, "A").start();
  47. new Thread(d2, "B").start();
  48. }
  49. }

运行结果如下图所示:
QQ截图20200710232709.png
上述代码块中创建了 A 和 B 两个线程,分别执行 run()方法中 if 和 else 代码块中的同步代码块。A 线程中拥有 a 锁,只有获得 b 锁才能执行完毕,而 B 线程拥有 b 锁,只有获得 a 锁才能执行完毕,两个线程都需要对方所占用的锁,但是都无法释放自己所拥有的锁,于是这两个线程都处于挂起状态,从而造成了上图所示的“死锁”。

如何避免死锁

教科书式的回答应该是,结合“哲学家就餐”模型,分析并总结出以下死锁的原因,最后得出“避免死锁就是破坏造成死锁的,若干条件的任意一个”的结论。

造成死锁必须达到的 4 个条件(原因):

  1. 互斥条件:一个资源每次只能被一个线程使用
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:线程已获得的资源,在未使用完之间,不能强行剥夺
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

但是,“哲学家就餐”模型光看名字就很讨厌,以上这 4 个条件看起来也很拗口,要想办法把这 4 个条件简化一下。

于是,通过对 4 个造成死锁的条件进行逐条分析,我们可以得到以下 4 个结论。

  1. 互斥条件 ——> 独占锁的特点之一
  2. 请求与保持条件 ——> 独占锁的特点之一,尝试获取锁时并不会释放已经持有的锁
  3. 不剥夺条件 ——> 独占锁的特点之一
  4. 循环等待条件 ——> 唯一需要记忆的造成死锁的条件

所以,如何避免死锁,只需要一句话:在并发程序中,避免逻辑中出现复数个线程互相持有对方线程所需要的独占锁的情况,就可以避免死锁。