Java多執行緒死鎖的產生原因以及如何避免
多執行緒以改善了系統資源的利用率並且提高了系統的處理能力。但是,併發執行同時也帶來了新的問題——死鎖。所謂的死鎖就是多個執行緒因競爭資源而造成的一種互相等待,如果沒有外力作用,這些執行緒都將無法繼續執行
死鎖產生的原因
系統資源的競爭
通常系統中擁有的不可剝奪資源,其數量不足以滿足多個執行緒執行的需要,使得執行緒在 執行過程中,會因爭奪資源而陷入僵局,如磁帶機、印表機等。只有對不可剝奪資源的競爭 才可能產生死鎖,對可剝奪資源的競爭是不會引起死鎖的。
執行緒推進順序非法
執行緒在執行過程中,請求和釋放資源的順序不當,也同樣會導致死鎖。例如,併發執行緒 P1、P2分別保持了資源R1、R2,而執行緒P1申請資源R2,執行緒P2申請資源R1時,兩者都 會因為所需資源被佔用而阻塞。
訊號量使用不當也會造成死鎖。執行緒間彼此相互等待對方發來的訊息,結果也會使得這 些執行緒間無法繼續向前推進。例如,執行緒A等待執行緒B發的訊息,執行緒B又在等待執行緒A 發的訊息,可以看出執行緒A和B不是因為競爭同一資源,而是在等待對方的資源導致死鎖。
死鎖產生的必要條件
產生死鎖必須同時滿足以下四個條件,只要其中任一條件不成立,死鎖就不會發生。
- 互斥條件:執行緒要求對所分配的資源(如印表機)進行排他性控制,即在一段時間內某資源僅為一個執行緒所佔有。此時若有其他執行緒請求該資源,則請求執行緒只能等待
- 不剝奪條件:執行緒所獲得的資源在未使用完畢之前,不能被其他執行緒強行奪走,即只能 由獲得該資源的執行緒自己來釋放(只能是主動釋放)
- 請求和保持條件:執行緒已經保持了至少一個資源,但又提出了新的資源請求,而該資源 已被其他執行緒佔有,此時請求執行緒被阻塞,但對自己已獲得的資源保持不放
- 迴圈等待條件:存在一種執行緒資源的迴圈等待鏈,鏈中每一個執行緒已獲得的資源同時被 鏈中下一個執行緒所請求。即存在一個處於等待狀態的執行緒集合{Pl, P2, ..., pn},其中Pi等 待的資源被P(i+1)佔有(i=0, 1, ..., n-1),Pn等待的資源被P0佔有
如何避免
在有些情況下死鎖是可以避免的。三種用於避免死鎖的技術:
- 加鎖順序
- 加鎖時限
- 死鎖檢測
加鎖順序
當多個執行緒需要相同的一些鎖,但是按照不同的順序加鎖,死鎖就容易發生。
按照順序加鎖是一種有效的死鎖預防機制。但是,這種方式需要事先知道所有可能會用到的鎖,但總有些時候是無法預知的。
加鎖時限
當一個執行緒在嘗試獲取鎖的過程中超過了這個時限則該執行緒應該放棄對該鎖進行請求。
若一個執行緒沒有在給定的時限內成功獲得所有需要的鎖,則會進行回退並釋放所有已經獲得的鎖,然後等待一段隨機的時間再重試。這段隨機的等待時間讓其它執行緒有機會嘗試獲取相同的這些鎖,並且讓該應用在沒有獲得鎖的時候可以繼續執行。
需要注意的是,由於存在鎖的超時,所以我們不能認為這種場景就一定是出現了死鎖。也可能是因為獲得了鎖的執行緒(導致其它執行緒超時)需要很長的時間去完成它的任務。
此外,如果有非常多的執行緒同一時間去競爭同一批資源,就算有超時和回退機制,還是可能會導致這些執行緒重複地嘗試但卻始終得不到鎖。如果只有兩個執行緒,並且重試的超時時間設定為0到500毫秒之間,這種現象可能不會發生,但是如果是10個或20個執行緒情況就不同了。因為這些執行緒等待相等的重試時間的概率就高的多(或者非常接近以至於會出現問題)。
死鎖檢測
死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖並且鎖超時也不可行的場景。
每當一個執行緒獲得了鎖,會線上程和鎖相關的資料結構中(map、graph等等)將其記下。除此之外,每當有執行緒請求鎖,也需要記錄在這個資料結構中。
當一個執行緒請求鎖失敗時,這個執行緒可以遍歷鎖的關係圖看看是否有死鎖發生。
那麼當檢測出死鎖時,這些執行緒該做些什麼呢?
一個可行的做法是釋放所有鎖,回退,並且等待一段隨機的時間後重試。這個和簡單的加鎖超時類似,不一樣的是隻有死鎖已經發生了才回退,而不會是因為加鎖的請求超時了。雖然有回退和等待,但是如果有大量的執行緒競爭同一批鎖,它們還是會重複地死鎖(編者注:原因同超時類似,不能從根本上減輕競爭)。
一個更好的方案是給這些執行緒設定優先順序,讓一個(或幾個)執行緒回退,剩下的執行緒就像沒發生死鎖一樣繼續保持著它們需要的鎖。如果賦予這些執行緒的優先順序是固定不變的,同一批執行緒總是會擁有更高的優先順序。為避免這個問題,可以在死鎖發生的時候設定隨機的優先順序。
Linux公社的RSS地址 :https://www.linuxidc.com/rssFeed.aspx
本文永久更新連結地址:https://www.linuxidc.com/Linux/2019-02/156796.htm