歡迎來到《併發王者課》,本文是該系列文章中的第12篇。
在上篇文章中,我們介紹了死鎖的概念及其原因,本文將為你介紹的是幾種常見的死鎖預防策略。
簡單來說,預防死鎖主要有三種策略:
- 順序化加鎖;
- 給鎖一個超時期限;
- 檢測死鎖。
一、順序化加鎖
通常,死鎖的產生是由於多個執行緒無序請求資源造成的。資源是有限的,不可能同時滿足所有執行緒的請求。然而,如果能按照一定的順序分別滿足各個執行緒的請求,那麼死鎖也就不再存在,也就是所謂的順序化加鎖(Lock Ordering)。
舉個通俗點的例子,煩人的路口堵車你一定遇到過。路口之所以堵車,是因為車太多了,大家都爭相往自己的方向去,互不相讓,不堵才怪。這時候,就需要交警在中間進行協調指揮,疏解擁堵。交警之所以可疏解擁堵,其根本原因在於,交警讓原本處於無序競爭的車流變成了井然有序的佇列。
車還是那麼多的車,路口還是那個路口,可是道路通暢了,這和執行緒的鎖競爭是類似的道理。
在上篇文章的死鎖程式碼中,執行緒1和執行緒2分別先佔有了A和B,導致了死鎖。按照剛才的思路,我們把順序調整下,執行緒1和執行緒2都先佔有A,然後再同時爭奪B,那麼死鎖就不會發生。
定義哪吒執行緒1,先搶佔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...");
}
}
}
}
定義蘭陵王執行緒2,也是先搶佔A再搶佔B,這與此前就不同了:
private static class LanLingWang 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...");
}
}
}
}
啟動兩個執行緒:
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...
哪吒: 已經同時持有A和B...
蘭陵王: 持有A!
蘭陵王: 等待B...
蘭陵王: 已經同時持有A和B...
從執行的結果中可以看到,兩個執行緒都先後獲得了自己所需要的資源,而沒有導致死鎖。
調整加鎖順序是一種簡單但有效的死鎖預防策略。但是,這一策略並不是萬能的,它僅適用於你在編碼時已經知曉加鎖的順序。
二、給鎖一個超時期限
在上篇文章中,我們說過死鎖的產生有一些必要的條件,其中一個是無限等待。設定鎖超時時間正是為了打破這一條件,讓無限等待變成有限等待。
仍然以前面的程式碼為例,哪吒和蘭陵王兩個執行緒在爭奪資源時,對方都互不相讓導致了無限等待的僵局。而此時,如果其中任何一方給等待設定一個期限,那麼時間一到,僵局將不攻自破,而執行緒仍可以再稍等片刻後繼續嘗試。
需要注意的是,synchronized
程式碼塊不可以指定鎖超時。所以,如果需要鎖超時,你需要使用自定義鎖,或者使用JDK提供的併發工具類。相關工具類的用法,會在後續文章中介紹,本文暫不展開描述。
另外,所謂給鎖加一個超時的期限,其實有兩層含義。一是在請求鎖時需要設定超時時間,二是在獲取鎖之後對鎖的持有也要有個超時時間,總不能到手就不放,那是耍流氓。
三、死鎖檢測
作為死鎖預防的第三種策略,你可以認為死鎖檢測(Deadlock Detection)是一項較重的被動技能,當我們無法順序化加鎖,也無法設定鎖的超時時間,那麼就需要進行死鎖檢測。
死鎖檢測的核心原理在於對執行緒和資源進行資料化打標和跟蹤。
線上程獲取鎖時,會將鎖和執行緒的對應關係通過Graph或者Map等資料結構記錄下來。這樣一來,執行緒在獲取鎖被拒絕時,可以通過遍歷已經記錄的資料分析是否存在死鎖。
當執行緒發現死鎖的情況後,可以採取釋放鎖,稍等片刻後再次嘗試。
附、如何視覺化檢視執行緒死鎖等狀態
在你感覺執行緒可能被阻塞或死鎖時,可以通過jstack
命令檢視。如果存在死鎖,輸出的結果中會有明確的死鎖提示,如下面所示:
$ jstack -F 8321
Attaching to process ID 8321, please wait...
Debugger attached successfully.
Client compiler detected.
JVM version is 1.6.0-rc-b100
Deadlock Detection:
Found one Java-level deadlock:
=============================
"Thread2":
waiting to lock Monitor@0x000af398 (Object@0xf819aa10, a java/lang/String),
which is held by "Thread1"
"Thread1":
waiting to lock Monitor@0x000af400 (Object@0xf819aa48, a java/lang/String),
which is held by "Thread2"
Found a total of 1 deadlock.
除了jstack
之外,JProfiler也是一款非常強大的執行緒與堆疊分析工具,並可以和IDEA等IDE完美結合。
藉助於JProfiler,我們可以非常直觀地看到上述示例程式碼中的死鎖,也可以在Thread Monitor中看到兩個執行緒的狀態為blocked.
需要注意的是,JProfiler是一款付費軟體,它提供了十天的免費試用時間。如果沒有常規的使用需求,而是僅用於學習的話,十天也是夠用的。當然,你也可以考慮使用jConsole、jVisualvm等。
小結
以上就是關於死鎖預防策略的全部內容。在本文中,我們介紹了三種死鎖預發策略。三種策略各有利弊,就實際工作中的應用而言,第二種給鎖設定超時期限是更為常用的一種做法,而第一種和第三種具有一定的邏輯難度和技術難度,更側重於理解而非實際應用。
正文到此結束,恭喜你又上了一顆星✨
夫子的試煉
- 通過
jstack
命令檢視死鎖並解決。
延伸閱讀與參考資料
關於作者
關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(儘量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。
如果本文對你有幫助,歡迎點贊、關注、監督,我們一起從青銅到王者。