黃金02:行穩致遠-如何讓你的執行緒免於死鎖

黃金02:行穩致遠-如何讓你的執行緒免於死鎖

歡迎來到《

併發王者課

》,本文是該系列文章中的

第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。。。

從執行的結果中可以看到,

兩個執行緒都先後獲得了自己所需要的資源,而沒有導致死鎖

調整加鎖順序是一種簡單但有效的死鎖預防策略。但是,這一策略並不是萬能的,它僅適用於你在編碼時已經知曉加鎖的順序。

二、給鎖一個超時期限

在上篇文章中,我們說過死鎖的產生有一些必要的條件,其中一個是

無限等待

。設定鎖超時時間正是為了打破這一條件,

讓無限等待變成有限等待

仍然以前面的程式碼為例,哪吒和蘭陵王兩個執行緒在爭奪資源時,對方都互不相讓導致了無限等待的僵局。而此時,如果其中任何一方給等待設定一個期限,那麼時間一到,僵局將不攻自破,而執行緒仍可以再稍等片刻後繼續嘗試。

黃金02:行穩致遠-如何讓你的執行緒免於死鎖

需要注意的是,

synchronized程式碼塊不可以指定鎖超時

。所以,如果需要鎖超時,你需要使用自定義鎖,或者使用JDK提供的併發工具類。相關工具類的用法,會在後續文章中介紹,本文暫不展開描述。

另外,所謂給鎖加一個超時的期限,其實有兩層含義。

一是在請求鎖時需要設定超時時間,二是在獲取鎖之後對鎖的持有也要有個超時時間,總不能到手就不放,那是耍流氓

三、死鎖檢測

作為死鎖預防的第三種策略,你可以認為

死鎖檢測(Deadlock Detection)是一項較重的被動技能

,當我們無法順序化加鎖,也無法設定鎖的超時時間,那麼就需要進行死鎖檢測。

死鎖檢測的核心原理在於對執行緒和資源進行資料化打標和跟蹤。

線上程獲取鎖時,會將鎖和執行緒的對應關係透過Graph或者Map等資料結構記錄下來。這樣一來,執行緒在獲取鎖被拒絕時,可以透過

遍歷

已經記錄的資料分析是否存在死鎖。

當執行緒發現死鎖的情況後,可以採取釋放鎖,稍等片刻後再次嘗試。

附、如何視覺化檢視執行緒死鎖等狀態

在你感覺執行緒可能被阻塞或死鎖時,可以透過

jstack

命令檢視。如果存在死鎖,輸出的結果中會有明確的死鎖提示,如下面所示:

$ jstack -F 8321Attaching to process ID 8321, please wait。。。Debugger attached successfully。Client compiler detected。JVM version is 1。6。0-rc-b100Deadlock 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

黃金02:行穩致遠-如何讓你的執行緒免於死鎖

黃金02:行穩致遠-如何讓你的執行緒免於死鎖

需要注意的是,

JProfiler是一款付費軟體

,它提供了十天的免費試用時間。如果沒有常規的使用需求,而是僅用於學習的話,十天也是夠用的。當然,你也可以考慮使用jConsole、jVisualvm等。

小結

以上就是關於死鎖預防策略的全部內容。在本文中,我們介紹了三種死鎖預發策略。三種策略各有利弊,就實際工作中的應用而言,

第二種給鎖設定超時期限是更為常用的一種做法

,而第一種和第三種具有一定的邏輯難度和技術難度,更側重於理解而非實際應用。

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

夫子的試煉

透過

jstack

命令檢視死鎖並解決。

延伸閱讀與參考資料

Deadlock Prevention

《併發王者課》大綱與更新進度總覽

關於作者

關注公眾號【

庸人技術笑談

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

如果本文對你有幫助,歡迎

點贊

關注

監督

,我們一起

從青銅到王者

關於作者

關注【

技術八點半

】,及時獲取文章更新。傳遞有品質的技術文章,早晨8:30推送作者品質原創,晚上20:30推送行業深度好文。

如果本文對你有幫助,歡迎

點贊

關注

監督

,我們一起

從青銅到王者