歡迎來到《併發王者課》,本文是該系列文章中的第11篇。
在本篇文章中,我將為你介紹多執行緒中的經典問題-死鎖,以及死鎖的產生原因、處理和方式預防措施。
一、死鎖的產生
觀察下面這幅圖,執行緒1持有了A,但它需要B;而執行緒2持有了B,但是它需要A。
你看,問題就來了,A、B都在等待對方已經持有的資源,並且都不釋放,這就讓事情陷入了僵局,也就是產生了死鎖。
在併發程式設計中,死鎖表示的是一種狀態。在這種狀態下,各方都在等待另一方釋放所持有的資源,但是它們之間又缺乏必要的通訊機制,導致彼此存在環路依賴而永遠地等待下去。
死鎖不僅存在於Java程式中,在諸如資料庫等其他中介軟體及分散式架構中都會存在。在資料的設計中,會考慮到死鎖的監測和恢復。當資料庫中發生死鎖時,將選擇一個犧牲者並放棄對應的事務,同時釋放鎖定的資源。在它的競爭者執行結束後,應用程式可以重新執行這個事務,因為它的競爭者此前已經完成事務。
然而,在JVM中,處理死鎖並沒有資料庫中那麼優雅。當一組執行緒發生死鎖時,“遊戲”將到此結束,這些執行緒將不能再使用,而這可能會直接導致應用程式崩潰、效能降低或者部分功能停止。
所以,和其他併發問題一樣,死鎖是危險的,死鎖造成的影響會立即表現出來,而如果在高負載情況下,這將是一場災難。
二、死鎖產生的必要條件
從第一小節的圖示中,我們可以看到死鎖產生的一些必要條件:
- 互斥:一個資源每次只能被一個執行緒使用。比如,上圖中的A和B同時只能被執行緒1和執行緒2其中一個使用;
- 請求與保持條件:一個執行緒在請求其他資源被阻塞時,對已經持有的資源保持不釋放。比如,上圖中的執行緒1在請求B時,並不會釋放A;
- 不剝奪條件:對於執行緒已經獲得的資源,在它主動釋放前,不可以主動剝奪。比如,上圖中執行緒1和執行緒2已經獲得的資源,除非自己釋放,否則不可以被強制剝奪;
- 迴圈等待條件:多個執行緒之間形成環狀等待。上圖中的執行緒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中具體的死鎖預發方式我們會在後面的文章中介紹。
小結
以上就是關於死鎖的全部內容。在本文中,我們介紹了什麼是死鎖,以及死鎖產生的必要條件和應對策略。對待開發中的死鎖問題,既要保持敬畏之心,也不必聞之色變,審慎分析死鎖的可能並設計合理策略可以有效預防死鎖。
正文到此結束,恭喜你又上了一顆星✨
夫子的試煉
- 執行本文的示例程式碼,嘗試找到破解其死鎖的方法。
延伸閱讀與參考資料
- 死鎖
- 《Java Concurrency in Practice》
- 死鎖預防演算法
- 《併發王者課》大綱與更新進度總覽
關於作者
關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(儘量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。
如果本文對你有幫助,歡迎點贊、關注、監督,我們一起從青銅到王者。