了解为什么在此实现中会发生死锁

Understanding why deadlock happens in this implementation

我是多线程的新手,遇到了这个例子:

public class TestThread {
   public static Object Lock1 = new Object();
   public static Object Lock2 = new Object();

   public static void main(String args[]) {

      ThreadDemo1 T1 = new ThreadDemo1();
      ThreadDemo2 T2 = new ThreadDemo2();
      T1.start();
      T2.start();
   }

   private static class ThreadDemo1 extends Thread {
      public void run() {
         synchronized (Lock1) {
            System.out.println("Thread 1: Holding lock 1...");
            try { Thread.sleep(10); }
            catch (InterruptedException e) {}
            System.out.println("Thread 1: Waiting for lock 2...");
            synchronized (Lock2) {
               System.out.println("Thread 1: Holding lock 1 & 2...");
            }
         }
      }
   }
   private static class ThreadDemo2 extends Thread {
      public void run() {
         synchronized (Lock2) {
            System.out.println("Thread 2: Holding lock 2...");
            try { Thread.sleep(10); }
            catch (InterruptedException e) {}
            System.out.println("Thread 2: Waiting for lock 1...");
            synchronized (Lock1) {
               System.out.println("Thread 2: Holding lock 1 & 2...");
            }
         }
      }
   } 
}

这会导致以下示例输出:

Thread 1: Holding lock 1...
Thread 2: Holding lock 2...
Thread 1: Waiting for lock 2...
Thread 2: Waiting for lock 1...

即出现死锁。但是,如果我们改变在第二个线程中获得锁的顺序,现在看起来像这样:

public class TestThread {
   public static Object Lock1 = new Object();
   public static Object Lock2 = new Object();

   public static void main(String args[]) {

      ThreadDemo1 T1 = new ThreadDemo1();
      ThreadDemo2 T2 = new ThreadDemo2();
      T1.start();
      T2.start();
   }

   private static class ThreadDemo1 extends Thread {
      public void run() {
         synchronized (Lock1) {
            System.out.println("Thread 1: Holding lock 1...");
            try { Thread.sleep(10); }
            catch (InterruptedException e) {}
            System.out.println("Thread 1: Waiting for lock 2...");
            synchronized (Lock2) {
               System.out.println("Thread 1: Holding lock 1 & 2...");
            }
         }
      }
   }
   private static class ThreadDemo2 extends Thread {
      public void run() {
         synchronized (Lock1) {
            System.out.println("Thread 2: Holding lock 1...");
            try { Thread.sleep(10); }
            catch (InterruptedException e) {}
            System.out.println("Thread 2: Waiting for lock 2...");
            synchronized (Lock2) {
               System.out.println("Thread 2: Holding lock 1 & 2...");
            }
         }
      }
   } 
}

它按预期工作,示例输出如下所示:

Thread 1: Holding lock 1...
Thread 1: Waiting for lock 2...
Thread 1: Holding lock 1 & 2...
Thread 2: Holding lock 1...
Thread 2: Waiting for lock 2...
Thread 2: Holding lock 1 & 2...

有人可以向我解释导致死锁的第一个代码中发生了什么,为什么第二个代码中的更改可以修复它?

好吧,在你的第一段代码中,第一个线程显然持有 Lock1,第二个线程持有 Lock2。然后它们试图获取彼此的锁,但它们失败了,因为其他锁已经被另一个线程持有,因此产生了死锁。

然而,在第二段代码中,第二个线程没有持有 Lock2 并且在第一个线程释放第一个锁之前被阻止。如您的输出所示,第一个线程在获取第二个锁后将释放第一个锁。

这是第一种情况的可能情况:

线程 1 获取 Lock1 并休眠 10 毫秒。现在线程 2 获取 Lock2 并进入休眠状态 10 毫秒。

现在线程 1 试图获取 Lock2,但无法获取,因为它已被线程 2 获取,而线程 2 试图获取被线程 1 锁定的 Lock1

在第二种情况下,我们假设第一个线程被选为 运行。它获得 Lock1,而另一个线程被阻塞,因为它也试图获得 Lock1。现在线程 1 进入睡眠状态,并继续执行第二个(嵌套的)同步块。它试图获得它(因为它仍然是免费的)——现在他获得了 2 锁。另一个线程仍然被阻塞。

只有在它完成执行后,它才会释放两个锁(因为它退出了同步块),现在JVM可以自由决定选择哪个线程,但它不会真的很重要,同样的逻辑也会发生。

您在这里看到的是锁排序,一种常见的死锁预防方法。

在第一种情况下,考虑以下执行顺序,当前指令指针位于每个线程的标记位置:

  Thread 1:
     obtain lock 1
===>
     obtain lock 2
  Thread 2:
     obtain lock 2
===>
     obtain lock 1

现在,线程 1 接下来尝试获取锁 2,但是不能,因为锁 2 被线程 2 持有。线程 2 接下来尝试获取锁 1,但是不能,因为它被线程 1 持有。这是经典的循环资源依赖,因此导致死锁。

防止这种情况的一种全局方法是确保所有锁都具有 顺序,并且始终以该总顺序获取锁。

这个工作的证据很简单:因为所有锁依赖项在总顺序中都是 "downward",所以你不能有锁周期。