发生死锁的 4 个必要条件

要想发生死锁有 4 个缺一不可的必要条件,我们一个个来看:

第 1 个叫互斥条件,它的意思是每个资源每次只能被一个线程(或进程,下同)使用,为什么资源不能同时被多个线程或进程使用呢?这是因为如果每个人都可以拿到想要的资源,那就不需要等待,所以是不可能发生死锁的。

第 2 个是请求与保持条件,它是指当一个线程因请求资源而阻塞时,则需对已获得的资源保持不放。如果在请求资源时阻塞了,并且会自动释放手中资源(例如锁)的话,那别人自然就能拿到我刚才释放的资源,也就不会形成死锁。

第 3 个是不剥夺条件,它是指线程已获得的资源,在未使用完之前,不会被强行剥夺。比如我们在上一课时中介绍的数据库的例子,它就有可能去强行剥夺某一个事务所持有的资源,这样就不会发生死锁了。所以要想发生死锁,必须满足不剥夺条件,也就是说当现在的线程获得了某一个资源后,别人就不能来剥夺这个资源,这才有可能形成死锁。

第 4 个是循环等待条件,只有若干线程之间形成一种头尾相接的循环等待资源关系时,才有可能形成死锁,比如在两个线程之间,这种“循环等待”就意味着它们互相持有对方所需的资源、互相等待;而在三个或更多线程中,则需要形成环路,例如依次请求下一个线程已持有的资源等。

案例解析
下面我们回到上一课时中所写的必然死锁的例子中,看看它是否一一满足了这 4 个条件,案例代码如下所示:

复制代码
 /**
  * 描述:     必定死锁的情况
  */
 public class MustDeadLock implements Runnable {    public int flag;
     static Object o1 = new Object();
     static Object o2 = new Object();    public void run() {
         System.out.println("线程"+Thread.currentThread().getName() + "的flag为" + flag);
         if (flag == 1) {
             synchronized (o1) {
                 try {
                     Thread.sleep(500);
                 } catch (Exception e) {
                     e.printStackTrace();
                 }
                 synchronized (o2) {
                     System.out.println("线程1获得了两把锁");
                 }
             }
         }
         if (flag == 2) {
             synchronized (o2) {
                 try {
                     Thread.sleep(500);
                 } catch (Exception e) {
                     e.printStackTrace();
                 }
                 synchronized (o1) {
                     System.out.println("线程2获得了两把锁");
                 }
             }
         }
     }    public static void main(String[] argv) {
         MustDeadLock r1 = new MustDeadLock();
         MustDeadLock r2 = new MustDeadLock();
         r1.flag = 1;
         r2.flag = 2;
         Thread t1 = new Thread(r1, "t1");
         Thread t2 = new Thread(r2, "t2");
         t1.start();
         t2.start();
     }
  }


这个代码的具体分析和执行结果,我们在上一课时中已经介绍过了,这里不重复讲解,下面我们把重点放在对这 4 个必要条件的分析上。

我们先来看一下第 1 个互斥条件,很显然,我们使用的是 synchronized 互斥锁,它的锁对象 o1、o2 只能同时被一个线程所获得,所以是满足互斥条件的。

第 2 个是请求与保持条件,可以看到,同样是满足的。比如,线程 1 在获得 o1 这把锁之后想去尝试获取 o2 这把锁 ,这时它被阻塞了,但是它并不会自动去释放 o1 这把锁,而是对已获得的资源保持不放。

java 查死锁 java 死锁的四个必要条件_System

第 3 个是不剥夺条件,在我们这个代码程序中,JVM 并不会主动把某一个线程所持有的锁剥夺,所以也满足不剥夺条件。

java 查死锁 java 死锁的四个必要条件_java_02

第 4 个是循环等待条件,可以看到在我们的例子中,这两个线程都想获取对方已持有的资源,也就是说线程 1 持有 o1 去等待 o2,而线程 2 则是持有 o2 去等待 o1,这是一个环路,此时就形成了一个循环等待。

java 查死锁 java 死锁的四个必要条件_java_03

可以看出,在我们的例子中确实满足这 4 个必要条件,今后我们就可以从这 4 个发生死锁的必要条件出发,来解决死锁的问题,只要破坏任意一个条件就可以消除死锁,这也是我们后面要讲的解决死锁策略中重点要考虑的内容。

 

==============如何定位死锁====================

在此之前,我们介绍了什么是死锁,以及死锁发生的必要条件。当然,即便我们很小心地编写代码,也必不可免地依然有可能会发生死锁,一旦死锁发生,第一步要做的就是把它给找到,因为在找到并定位到死锁之后,才能有接下来的补救措施,比如解除死锁、解除死锁之后恢复、对代码进行优化等;若找不到死锁的话,后面的步骤就无从谈起了。

下面就来看一下是如何用命令行的方式找到死锁的。

命令:jstack

这个命令叫作 jstack,它能看到我们 Java 线程的一些相关信息。如果是比较明显的死锁关系,那么这个工具就可以直接检测出来;如果死锁不明显,那么它无法直接检测出来,不过我们也可以借此来分析线程状态,进而就可以发现锁的相互依赖关系,所以这也是很有利于我们找到死锁的方式。

我们就来试一试,执行这个命令。

复制代码
 /**
  * 描述:     必定死锁的情况
  */
 public class MustDeadLock implements Runnable {    public int flag;
     static Object o1 = new Object();
     static Object o2 = new Object();     public void run() {
         System.out.println("线程"+Thread.currentThread().getName() + "的flag为" + flag);
         if (flag == 1) {
             synchronized (o1) {
                 try {
                     Thread.sleep(500);
                 } catch (Exception e) {
                     e.printStackTrace();
                 }
                 synchronized (o2) {
                     System.out.println("线程1获得了两把锁");
                 }
             }
         }
         if (flag == 2) {
             synchronized (o2) {
                 try {
                     Thread.sleep(500);
                 } catch (Exception e) {
                     e.printStackTrace();
                 }
                 synchronized (o1) {
                     System.out.println("线程2获得了两把锁");
                 }
             }
         }
     }    public static void main(String[] argv) {
         MustDeadLock r1 = new MustDeadLock();
         MustDeadLock r2 = new MustDeadLock();
         r1.flag = 1;
         r2.flag = 2;
         Thread t1 = new Thread(r1, "t1");
         Thread t2 = new Thread(r2, "t2");
         t1.start();
         t2.start();
      }
 }


由于它发生了死锁,在我们没有干预的情况下,程序在运行后就不会停止;然后打开我们的终端,执行 ${JAVA_HOME}/bin/jps 这个命令,就可以查看到当前 Java 程序的 pid,我的执行结果如下:

复制代码
56402 MustDeadLock
56403 Launcher
56474 Jps
55051 KotlinCompileDaemon
有多行,可以看到第一行是 MustDeadLock 这类的 pid 56402;然后我们继续执行下一个命令,${JAVA_HOME}/bin/jstack 加空格,接着输入我们刚才所拿到的这个类的 pid,也就是 56402,所以完整的命令是 ${JAVA_HOME}/bin/jstack 56402;最后它会打印出很多信息,就包含了线程获取锁的信息,比如哪个线程获取哪个锁,它获得的锁是在哪个语句中获得的,它正在等待或者持有的锁是什么等,这些重要信息都会打印出来。我们截取一部分和死锁相关的有用信息,展示如下:

复制代码

Found one Java-level deadlock:
 =============================
 "t2":
   waiting to lock monitor 0x00007fa06c004a18 (object 0x000000076adabaf0, a java.lang.Object),
   which is held by "t1"
 "t1":
   waiting to lock monitor 0x00007fa06c007358 (object 0x000000076adabb00, a java.lang.Object),
   which is held by "t2"Java stack information for the threads listed above:
 ===================================================
 "t2":
     at lesson67.MustDeadLock.run(MustDeadLock.java:31)
     - waiting to lock <0x000000076adabaf0> (a java.lang.Object)
     - locked <0x000000076adabb00> (a java.lang.Object)
     at java.lang.Thread.run(Thread.java:748)
 "t1":
     at lesson67.MustDeadLock.run(MustDeadLock.java:19)
     - waiting to lock <0x000000076adabb00> (a java.lang.Object)
     - locked <0x000000076adabaf0> (a java.lang.Object)
     at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock
在这里它首先会打印“Found one Java-level deadlock”,表明“找到了一个死锁”,然后是更详细的信息,从中间这部分的信息中可以看出,t2 线程想要去获取这个尾号为 af0 的锁对象,但是它被 t1 线程持有,同时 t2 持有尾号为 b00 的锁对象;相反,t1 想要获取尾号为 b00 的锁对象,但是它被 t2 线程持有,同时 t1 持有的却是尾号为 af0 的锁对象,这就形成了一个依赖环路,发生了死锁。最后它还打印出了“Found 1 deadlock.”,可以看出,jstack 工具不但帮我们找到了死锁,甚至还把哪个线程、想要获取哪个锁、形成什么样的环路都告诉我们了,当我们有了这样的信息之后,死锁就非常容易定位了,所以接下来我们就可以进一步修改代码,来避免死锁了。

以上就是利用 jstack 来定位死锁的方法,jstack 可以用来帮助我们分析线程持有的锁和需要的锁,然后分析出是否有循环依赖形成死锁的情况。

代码:ThreadMXBean

下面我们再看一下用代码来定位死锁的方式。

我们会用到 ThreadMXBean 工具类,代码示例如下:

复制代码

public class DetectDeadLock implements Runnable {
    public int flag;
     static Object o1 = new Object();
     static Object o2 = new Object();     public void run() {
         System.out.println(Thread.currentThread().getName()+" flag = " + flag);
         if (flag == 1) {
             synchronized (o1) {
                 try {
                     Thread.sleep(500);
                 } catch (Exception e) {
                     e.printStackTrace();
                 }
                 synchronized (o2) {
                     System.out.println("线程1获得了两把锁");
                 }
             }
         }
         if (flag == 2) {
             synchronized (o2) {
                 try {
                     Thread.sleep(500);
                 } catch (Exception e) {
                     e.printStackTrace();
                 }
                 synchronized (o1) {
                     System.out.println("线程2获得了两把锁");
                 }
             }
         }
     }    public static void main(String[] argv) throws InterruptedException {
         DetectDeadLock r1 = new DetectDeadLock();
         DetectDeadLock r2 = new DetectDeadLock();
         r1.flag = 1;
         r2.flag = 2;
         Thread t1 = new Thread(r1,"t1");
         Thread t2 = new Thread(r2,"t2");
         t1.start();
         t2.start();
         Thread.sleep(1000);
         ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
         long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
         if (deadlockedThreads != null && deadlockedThreads.length > 0) {
             for (int i = 0; i < deadlockedThreads.length; i++) {
                 ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
                 System.out.println("线程id为"+threadInfo.getThreadId()+",线程名为" + threadInfo.getThreadName()+"的线程已经发生死锁,需要的锁正被线程"+threadInfo.getLockOwnerName()+"持有。");
             }
         }
     }
 }


这个类是在前面 MustDeadLock 类的基础上做了升级,MustDeadLock 类的主要作用就是让线程 1 和线程 2 分别以不同的顺序来获取到 o1 和 o2 这两把锁,并且形成死锁。在 main 函数中,在启动 t1 和 t2 之后的代码,是我们本次新加入的代码,我们用 Thread.sleep(1000) 来确保已经形成死锁,然后利用 ThreadMXBean 来检查死锁。

通过 ThreadMXBean 的 findDeadlockedThreads 方法,可以获取到一个 deadlockedThreads 的数组,然后进行判断,当这个数组不为空且长度大于 0 的时候,我们逐个打印出对应的线程信息。比如我们打印出了线程 id,也打印出了线程名,同时打印出了它所需要的那把锁正被哪个线程所持有,那么这一部分代码的运行结果如下。

复制代码
t1 flag = 1
t2 flag = 2
线程 id 为 12,线程名为 t2 的线程已经发生死锁,需要的锁正被线程 t1 持有。
线程 id 为 11,线程名为 t1 的线程已经发生死锁,需要的锁正被线程 t2 持有。
一共有四行语句,前两行是“t1 flag = 1“、“t2 flag = 2”,这是发生死锁之前所打印出来的内容;然后的两行语句就是我们检测到的死锁的结果,可以看到,它打印出来的是“线程 id 为 12,线程名为 t2 的线程已经发生了死锁,需要的锁正被线程 t1 持有。”同样的,它也会打印出“线程 id 为 11,线程名为 t1 的线程已经发生死锁,需要的锁正被线程 t2 持有。”

可以看出,ThreadMXBean 也可以帮我们找到并定位死锁,如果我们在业务代码中加入这样的检测,那我们就可以在发生死锁的时候及时地定位,同时进行报警等其他处理,也就增强了我们程序的健壮性。