併發王者課-黃金1:兩敗俱傷-互不相讓的執行緒如何導致了死鎖僵局

秦二爺發表於2021-06-10

歡迎來到《併發王者課》,本文是該系列文章中的第11篇

在本篇文章中,我將為你介紹多執行緒中的經典問題-死鎖,以及死鎖的產生原因、處理和方式預防措施

一、死鎖的產生

觀察下面這幅圖,執行緒1持有了A,但它需要B;而執行緒2持有了B,但是它需要A。

你看,問題就來了,A、B都在等待對方已經持有的資源,並且都不釋放,這就讓事情陷入了僵局,也就是產生了死鎖

在併發程式設計中,死鎖表示的是一種狀態。在這種狀態下,各方都在等待另一方釋放所持有的資源,但是它們之間又缺乏必要的通訊機制,導致彼此存在環路依賴而永遠地等待下去。

死鎖不僅存在於Java程式中,在諸如資料庫等其他中介軟體及分散式架構中都會存在。在資料的設計中,會考慮到死鎖的監測和恢復。當資料庫中發生死鎖時,將選擇一個犧牲者並放棄對應的事務,同時釋放鎖定的資源。在它的競爭者執行結束後,應用程式可以重新執行這個事務,因為它的競爭者此前已經完成事務。

然而,在JVM中,處理死鎖並沒有資料庫中那麼優雅。當一組執行緒發生死鎖時,“遊戲”將到此結束,這些執行緒將不能再使用,而這可能會直接導致應用程式崩潰、效能降低或者部分功能停止

所以,和其他併發問題一樣,死鎖是危險的,死鎖造成的影響會立即表現出來,而如果在高負載情況下,這將是一場災難

二、死鎖產生的必要條件

從第一小節的圖示中,我們可以看到死鎖產生的一些必要條件:

  1. 互斥:一個資源每次只能被一個執行緒使用。比如,上圖中的A和B同時只能被執行緒1和執行緒2其中一個使用;
  2. 請求與保持條件:一個執行緒在請求其他資源被阻塞時,對已經持有的資源保持不釋放。比如,上圖中的執行緒1在請求B時,並不會釋放A;
  3. 不剝奪條件:對於執行緒已經獲得的資源,在它主動釋放前,不可以主動剝奪。比如,上圖中執行緒1和執行緒2已經獲得的資源,除非自己釋放,否則不可以被強制剝奪;
  4. 迴圈等待條件:多個執行緒之間形成環狀等待。上圖中的執行緒1和執行緒2所形成的就是迴圈等待。

三、模擬並體驗死鎖

在瞭解什麼是死鎖及其產生的條件後,我們根據上圖中的死鎖情景,通過一段程式碼來模擬體驗死鎖的發生。

根據上圖所示,定義哪吒執行緒,在執行時將持有A並請求B

private static class NeZha implements Runnable {
  public void run() {
    synchronized(lockA) {
      System.out.println("哪吒: 持有A!");

      try {
        Thread.sleep(10);
      } catch (InterruptedException ignored) {}
      System.out.println("哪吒: 等待B...");

      synchronized(lockB) {
        System.out.println("哪吒: 已經同時持有A和B...");
      }
    }
  }
}

定義蘭陵王執行緒,在執行時持有B並請求A

private static class LanLingWang implements Runnable {
  public void run() {
    synchronized(lockB) {
      System.out.println("蘭陵王: 持有B!");

      try {
        Thread.sleep(10);
      } catch (InterruptedException ignored) {}
      System.out.println("蘭陵王: 等待A...");

      synchronized(lockA) {
        System.out.println("蘭陵王: 已經同時持有A和B...");
      }
    }
  }
}

啟動兩個執行緒:

public class DeadLockDemo {
    public static final Object lockA = new Object();
    public static final Object lockB = new Object();

    public static void main(String args[]) {
        Thread thread1 = new Thread(new NeZha());
        Thread thread2 = new Thread(new LanLingWang());
        thread1.start();
        thread2.start();
    }
}

兩個執行緒的輸出結果如下:

哪吒: 持有A!
蘭陵王: 持有B!
哪吒: 等待B...
蘭陵王: 等待A...

從結果中可以看到,哪吒和蘭陵王分別持有了A和B,但他們又相互請求對方持有的資源,最終導致死鎖,兩個執行緒進入了無限地等待

四、死鎖的處理

1. 忽略死鎖

忽略死鎖是一種鴕鳥政策,它假設永遠不會發生死鎖。這種策略適用於死鎖發生概率較低且影響可容忍的場景,如果死鎖被證明永遠不會發生也可以採用這種策略

2. 檢測

在這種策略下,死鎖是允許發生的。如果系統檢測到死鎖,也會對其進行糾正,比如跟蹤執行緒狀態和資源分配。在死鎖時,可以通過一些方法進行糾正:

  • 執行緒終止:選擇其中一個或多個執行緒進行終止,釋放資源,打破死鎖狀態;
  • 資源搶佔:重新分配各執行緒已經搶佔的資源,直到打破死鎖。

3. 預防

對待死鎖問題,預防是關鍵。本文第二小節已經列舉死鎖產生的一些必要條件,所以如果要預防死鎖,只要打破其中任一條件即可,Java中具體的死鎖預發方式我們會在後面的文章中介紹。

小結

以上就是關於死鎖的全部內容。在本文中,我們介紹了什麼是死鎖,以及死鎖產生的必要條件和應對策略。對待開發中的死鎖問題,既要保持敬畏之心,也不必聞之色變,審慎分析死鎖的可能並設計合理策略可以有效預防死鎖。

正文到此結束,恭喜你又上了一顆星✨

夫子的試煉

  • 執行本文的示例程式碼,嘗試找到破解其死鎖的方法。

延伸閱讀與參考資料

關於作者

關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(儘量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。

如果本文對你有幫助,歡迎點贊關注監督,我們一起從青銅到王者

相關文章