概述
什麼是死鎖?
死鎖是這樣一種情形:多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放.由於線程被無限期地阻塞,因此程序不能正常運行.形象的說就是:一個寶藏需要兩把鑰匙來打開,同時間正好來了兩個人,他們一人一把鑰匙,但是雙方都再等著對方能交出鑰匙來打開寶藏,誰都沒釋放自己的那把鑰匙.就這樣這倆人一直僵持下去,直到開發人員發現這個局面.
導致死鎖的根源在於不適當地運用“synchronized”關鍵詞來管理線程對特定對象的訪問.“synchronized”關鍵詞的作用是,確保在某個時刻只有一個線程被允許執行特定的代碼塊,因此,被允許執行的線程首先必須擁有對變量或對象的排他性訪問權.當線程訪問對象時,線程會給對象加鎖,而這個鎖導致其它也想訪問同一對象的線程被阻塞,直至第一個線程釋放它加在對象上的鎖.
一丶JAVA中導致死鎖的原因
學過操作系統的朋友都知道:產生死鎖的條件有四個:
互斥條件:所謂互斥就是進程在某一時間內獨佔資源。
請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
不剝奪條件:進程已獲得資源,在末使用完之前,不能強行剝奪。
循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。
例如:
Java中死鎖最簡單的情況是,一個線程T1持有鎖L1並且申請獲得鎖L2,而另一個線程T2持有鎖L2並且申請獲得鎖L1,因為默認的鎖申請操作都是阻塞的,所以線程T1和T2永遠被阻塞了。導致了死鎖。這是最容易理解也是最簡單的死鎖的形式。
但是實際環境中的死鎖往往比這個複雜的多。可能會有多個線程形成了一個死鎖的環路,比如:線程T1持有鎖L1並且申請獲得鎖L2,而線程T2持有鎖L2並且申請獲得鎖L3,而線程T3持有鎖L3並且申請獲得鎖L1,這樣導致了一個鎖依賴的環路:T1依賴T2的鎖L2,T2依賴T3的鎖L3,而T3依賴T1的鎖L1。從而導致了死鎖。
從這兩個例子,我們可以得出結論,產生死鎖可能性的最根本原因是:線程在獲得一個鎖L1的情況下再去申請另外一個鎖L2,也就是鎖L1想要包含了鎖L2,也就是說在獲得了鎖L1,並且沒有釋放鎖L1的情況下,又去申請獲得鎖L2,這個是產生死鎖的最根本原因。另一個原因是默認的鎖申請操作是阻塞的。
二丶Java中如何避免死鎖
既然我們知道了產生死鎖可能性的原因,那麼就可以在編碼時進行規避。Java是面向對象的編程語言,程序的最小單元是對象,對象封裝了數據和操作,所以Java中的鎖一般也是以對象為單位的,對象的內置鎖保護對象中的數據的併發訪問。所以如果我們能夠避免在對象的同步方法中調用其它對象的同步方法,那麼就可以避免死鎖產生的可能性。如下所示的代碼,就存在死鎖的可能性:
public class ClassB {
private String address;
// ...
public synchronized void method1(){
// do something
}
// ... ...
}
public class ClassA {
private int id;
private String name;
private ClassB b;
// ...
public synchronized void m1(){
// do something
b.method1();
}
// ... ...
}
上面的ClassA.m1()方法,在對象的同步方法中又調用了ClassB的同步方法method1(),所以存在死鎖發生的可能性。我們可以修改如下,避免死鎖:
public class ClassA {
private int id;
private String name;
private ClassB b;
// ...
public void m2(){
synchronized(this){
// do something
}
b.method1();
}
// ... ...
}
這樣的話減小了鎖定的範圍,兩個鎖的申請就沒有發生交叉,避免了死鎖的可能性,這是最理性的情況,因為鎖沒有發生交叉。但是有時是不允許我們這樣做的。此時,如果只有ClassA中只有一個m1這樣的方法,需要同時獲得兩個對象上的鎖,並且不會將實例屬性 b 溢出(return b;),而是將實例屬性 b 封閉在對象中,那麼也不會發生死鎖。因為無法形成死鎖的閉環。但是如果ClassA中有多個方法需要同時獲得兩個對象上的鎖,那麼這些方法就必須以相同的順序獲得鎖。
比如銀行轉賬的場景下,我們必須同時獲得兩個賬戶上的鎖,才能進行操作,兩個鎖的申請必須發生交叉。這時我們也可以打破死鎖的那個閉環,在涉及到要同時申請兩個鎖的方法中,總是以相同的順序來申請鎖,比如總是先申請 id 大的賬戶上的鎖 ,然後再申請 id 小的賬戶上的鎖,這樣就無法形成導致死鎖的那個閉環。
public class Account {
private int id; // 主鍵
private String name;
private double balance;
public void transfer(Account from, Account to, double money){
if(from.getId() > to.getId()){
synchronized(from){
synchronized(to){
// transfer
}
}
}else{
synchronized(to){
synchronized(from){
// transfer
}
}
}
}
public int getId() {
return id;
}
}
這樣的話,即使發生了兩個賬戶比如 id=1的和id=100的兩個賬戶相互轉賬,因為不管是哪個線程先獲得了id=100上的鎖,另外一個線程都不會去獲得id=1上的鎖(因為他沒有獲得id=100上的鎖),只能是哪個線程先獲得id=100上的鎖,哪個線程就先進行轉賬。這裡除了使用id之外,如果沒有類似id這樣的屬性可以比較,那麼也可以使用對象的hashCode()的值來進行比較。
上面我們說到,死鎖的另一個原因是默認的鎖申請操作是阻塞的,所以如果我們不使用默認阻塞的鎖,也是可以避免死鎖的。我們可以使用ReentrantLock.tryLock()方法,在一個循環中,如果tryLock()返回失敗,那麼就釋放以及獲得的鎖,並睡眠一小段時間。這樣就打破了死鎖的閉環。
比如:線程T1持有鎖L1並且申請獲得鎖L2,而線程T2持有鎖L2並且申請獲得鎖L3,而線程T3持有鎖L3並且申請獲得鎖L1
此時如果T3申請鎖L1失敗,那麼T3釋放鎖L3,並進行睡眠,那麼T2就可以獲得L3了,然後T2執行完之後釋放L2, L3,所以T1也可以獲得L2了執行完然後釋放鎖L1, L2,然後T3睡眠醒來,也可以獲得L1, L3了。打破了死鎖的閉環。
這些情況,都還是比較好處理的,因為它們都是相關的,我們很容易意識到這裡有發生死鎖的可能性,從而可以加以防備。很多情況的場景都不會很明顯的讓我們察覺到會存在發生死鎖的可能性。
三丶死鎖出現的原因
當我們瞭解在什麼情況下會產生死鎖,以及什麼是死鎖的時候,我們在寫代碼的時候應該儘量的去避免這個誤區.產生死鎖必須同時滿足以下四個條件,只要其中任一條件不成立,死鎖就不會發生.
互斥條件:線程要求對所分配的資源進行排他性控制,即在一段時間內某 資源僅為一個進程所佔有.此時若有其他進程請求該資源.則請求進程只能等待.
不剝奪條件:進程所獲得的資源在未使用完畢之前,不能被其他進程強行奪走,即只能由獲得該資源的線程自己來釋放(只能是主動釋放).
請求和保持條件:線程已經保持了至少一個資源,但又提出了新的資源請求,而該資源已被其他線程佔有,此時請求線程被阻塞,但對自己已獲得的資源保持不放.
循環等待條件:存在一種線程資源的循環等待鏈,鏈中每一個線程已獲得的資源同時被鏈中下一個線程所請求。
四丶死鎖的解決方法
說實話避免死鎖還得再自己寫代碼的時候注意一下.這裡引用別人的解決方法,不過我對於這些解決方法不是太懂,講的太含糊沒有具體的實例.
總結
到這裡,Java多線程死鎖出現問題與如何解決方案就結束了,,不足之處還望大家多多包涵!!覺得收穫的話可以點個關注收藏轉發一波喔,謝謝大佬們支持。(吹一波,233~~)
下面和大家交流幾點編程的經驗:
1、多寫多敲代碼,好的代碼與紮實的基礎知識一定是實踐出來的
2丶 測試、測試再測試,如果你不徹底測試自己的代碼,那恐怕你開發的就不只是代碼,可能還會聲名狼藉。
3丶 簡化編程,加快速度,代碼風騷,在你完成編碼後,應回頭並且優化它。從長遠來看,這裡或那裡一些的改進,會讓後來的支持人員更加輕鬆。
最後,每一位讀到這裡的網友,感謝你們能耐心地看完。希望在成為一名更優秀的Java程序員的道路上,我們可以一起學習、一起進步。
內部交流群469717771 歡迎各位前來交流和分享, 驗證:(007)