這種情況可能發生在當兩個線程嘗試獲取其他資源的鎖,而每個線程又陷入無線等待其他資源鎖的釋放,除非一個用戶的進程被終止。
線程死鎖可能發生在以下的情況:
當兩個線程相互調用Thread.join();
當兩個線程使用嵌套的同步塊時,一個線程占用了另一個線程的必需的鎖,互相等待時被阻塞,就有可能出現死鎖。
舉例:在一個單軌的鐵路上,兩條火車向不同的方向開,誰也動不了。
為什么會出現死鎖?
死鎖一般都是由於對共享資源的競爭所引起的。但對共享資源的競爭又不一定就會發生死鎖。
死鎖的發生必需滿足4個必要條件:
互斥
等待/持有
非搶占
形成等待環
一、死鎖的定義
多線程以及多進程改善了系統資源的利用率並提高了系統 的處理能力。然而,並發執行也帶來了新的問題——死鎖。所謂死鎖是指多個線程因競爭資源而造成的一種僵局(互相等待),若無外力作用,這些進程都將無法向前推進。
下面我們通過一些實例來說明死鎖現象。
先看生活中的一個實例,2個人一起吃飯但是只有一雙筷子,2人輪流吃(同時擁有2只筷子才能吃)。某一個時候,一個拿了左筷子,一人拿了右筷子,2個人都同時占用一個資源,等待另一個資源,這個時候甲在等待乙吃完並釋放它占有的筷子,同理,乙也在等待甲吃完並釋放它占有的筷子,這樣就陷入了一個死循環,誰也無法繼續吃飯。。。
在計算機系統中也存在類似的情況。例如,某計算機系統中只有一台打印機和一台輸入 設備,進程P1正占用輸入設備,同時又提出使用打印機的請求,但此時打印機正被進程P2 所占用,而P2在未釋放打印機之前,又提出請求使用正被P1占用着的輸入設備。這樣兩個進程相互無休止地等待下去,均無法繼續執行,此時兩個進程陷入死鎖狀態。
二、死鎖產生的原因
1) 系統資源的競爭
通常系統中擁有的不可剝奪資源,其數量不足以滿足多個進程運行的需要,使得進程在 運行過程中,會因爭奪資源而陷入僵局,如磁帶機、打印機等。只有對不可剝奪資源的競爭 才可能產生死鎖,對可剝奪資源的競爭是不會引起死鎖的。
2) 進程推進順序非法
進程在運行過程中,請求和釋放資源的順序不當,也同樣會導致死鎖。例如,並發進程 P1、P2分別保持了資源R1、R2,而進程P1申請資源R2,進程P2申請資源R1時,兩者都 會因為所需資源被占用而阻塞。
信號量使用不當也會造成死鎖。進程間彼此相互等待對方發來的消息,結果也會使得這 些進程間無法繼續向前推進。例如,進程A等待進程B發的消息,進程B又在等待進程A 發的消息,可以看出進程A和B不是因為競爭同一資源,而是在等待對方的資源導致死鎖。
3) 死鎖產生的必要條件
產生死鎖必須同時滿足以下四個條件,只要其中任一條件不成立,死鎖就不會發生。
互斥條件:進程要求對所分配的資源(如打印機)進行排他性控制,即在一段時間內某 資源僅為一個進程所占有。此時若有其他進程請求該資源,則請求進程只能等待。
不剝奪條件:進程所獲得的資源在未使用完畢之前,不能被其他進程強行奪走,即只能 由獲得該資源的進程自己來釋放(只能是主動釋放)。
請求和保持條件:進程已經保持了至少一個資源,但又提出了新的資源請求,而該資源 已被其他進程占有,此時請求進程被阻塞,但對自己已獲得的資源保持不放。
循環等待條件:存在一種進程資源的循環等待鏈,鏈中每一個進程已獲得的資源同時被 鏈中下一個進程所請求。即存在一個處於等待狀態的進程集合{Pl, P2, ..., pn},其中Pi等 待的資源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的資源被P0占有,如圖2-15所示。
直觀上看,循環等待條件似乎和死鎖的定義一樣,其實不然。按死鎖定義構成等待環所 要求的條件更嚴,它要求Pi等待的資源必須由P(i+1)來滿足,而循環等待條件則無此限制。 例如,系統中有兩台輸出設備,P0占有一台,PK占有另一台,且K不屬於集合{0, 1, ..., n}。
Pn等待一台輸出設備,它可以從P0獲得,也可能從PK獲得。因此,雖然Pn、P0和其他 一些進程形成了循環等待圈,但PK不在圈內,若PK釋放了輸出設備,則可打破循環等待, 如圖2-16所示。因此循環等待只是死鎖的必要條件。
三、如何避免死鎖
在有些情況下死鎖是可以避免的。三種用於避免死鎖的技術:
加鎖順序(線程按照一定的順序加鎖)
加鎖時限(線程嘗試獲取鎖的時候加上一定的時限,超過時限則放棄對該鎖的請求,並釋放自己占有的鎖)
死鎖檢測
加鎖順序
當多個線程需要相同的一些鎖,但是按照不同的順序加鎖,死鎖就很容易發生。
如果能確保所有的線程都是按照相同的順序獲得鎖,那么死鎖就不會發生。看下面這個例子:
Thread 1: lock A lock B Thread 2: wait for A lock C (when A locked) Thread 3: wait for A wait for B wait for C
如果一個線程(比如線程3)需要一些鎖,那么它必須按照確定的順序獲取鎖。它只有獲得了從順序上排在前面的鎖之后,才能獲取后面的鎖。
例如,線程2和線程3只有在獲取了鎖A之后才能嘗試獲取鎖C(譯者注:獲取鎖A是獲取鎖C的必要條件)。因為線程1已經擁有了鎖A,所以線程2和3需要一直等到鎖A被釋放。然后在它們嘗試對B或C加鎖之前,必須成功地對A加了鎖。
按照順序加鎖是一種有效的死鎖預防機制。但是,這種方式需要你事先知道所有可能會用到的鎖(譯者注:並對這些鎖做適當的排序),但總有些時候是無法預知的。
加鎖時限
另外一個可以避免死鎖的方法是在嘗試獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程中若超過了這個時限該線程則放棄對該鎖請求。若一個線程沒有在給定的時限內成功獲得所有需要的鎖,則會進行回退並釋放所有已經獲得的鎖,然后等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,並且讓該應用在沒有獲得鎖的時候可以繼續運行(譯者注:加鎖超時后可以先繼續運行干點其它事情,再回頭來重復之前加鎖的邏輯)。
以下是一個例子,展示了兩個線程以不同的順序嘗試獲取相同的兩個鎖,在發生超時后回退並重試的場景:
Thread 1 locks A Thread 2 locks B Thread 1 attempts to lock B but is blocked Thread 2 attempts to lock A but is blocked Thread 1's lock attempt on B times out Thread 1 backs up and releases A as well Thread 1 waits randomly (e.g. 257 millis) before retrying. Thread 2's lock attempt on A times out Thread 2 backs up and releases B as well Thread 2 waits randomly (e.g. 43 millis) before retrying.
需要注意的是,由於存在鎖的超時,所以我們不能認為這種場景就一定是出現了死鎖。也可能是因為獲得了鎖的線程(導致其它線程超時)需要很長的時間去完成它的任務。
此外,如果有非常多的線程同一時間去競爭同一批資源,就算有超時和回退機制,還是可能會導致這些線程重復地嘗試但卻始終得不到鎖。如果只有兩個線程,並且重試的超時時間設定為0到500毫秒之間,這種現象可能不會發生,但是如果是10個或20個線程情況就不同了。因為這些線程等待相等的重試時間的概率就高的多(或者非常接近以至於會出現問題)。
(譯者注:超時和重試機制是為了避免在同一時間出現的競爭,但是當線程很多時,其中兩個或多個線程的超時時間一樣或者接近的可能性就會很大,因此就算出現競爭而導致超時后,由於超時時間一樣,它們又會同時開始重試,導致新一輪的競爭,帶來了新的問題。)
這種機制存在一個問題,在Java中不能對synchronized同步塊設置超時時間。你需要創建一個自定義鎖,或使用Java5中java.util.concurrent包下的工具。寫一個自定義鎖類不復雜,但超出了本文的內容。后續的Java並發系列會涵蓋自定義鎖的內容。
死鎖檢測
死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖並且鎖超時也不可行的場景。
每當一個線程獲得了鎖,會在線程和鎖相關的數據結構中(map、graph等等)將其記下。除此之外,每當有線程請求鎖,也需要記錄在這個數據結構中。
當一個線程請求鎖失敗時,這個線程可以遍歷鎖的關系圖看看是否有死鎖發生。例如,線程A請求鎖7,但是鎖7這個時候被線程B持有,這時線程A就可以檢查一下線程B是否已經請求了線程A當前所持有的鎖。如果線程B確實有這樣的請求,那么就是發生了死鎖(線程A擁有鎖1,請求鎖7;線程B擁有鎖7,請求鎖1)。
當然,死鎖一般要比兩個線程互相持有對方的鎖這種情況要復雜的多。線程A等待線程B,線程B等待線程C,線程C等待線程D,線程D又在等待線程A。線程A為了檢測死鎖,它需要遞進地檢測所有被B請求的鎖。從線程B所請求的鎖開始,線程A找到了線程C,然后又找到了線程D,發現線程D請求的鎖被線程A自己持有着。這是它就知道發生了死鎖。
下面是一幅關於四個線程(A,B,C和D)之間鎖占有和請求的關系圖。像這樣的數據結構就可以被用來檢測死鎖
那么當檢測出死鎖時,這些線程該做些什么呢?
一個可行的做法是釋放所有鎖,回退,並且等待一段隨機的時間后重試。這個和簡單的加鎖超時類似,不一樣的是只有死鎖已經發生了才回退,而不會是因為加鎖的請求超時了。雖然有回退和等待,但是如果有大量的線程競爭同一批鎖,它們還是會重復地死鎖(編者注:原因同超時類似,不能從根本上減輕競爭)。
一個更好的方案是給這些線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖一樣繼續保持着它們需要的鎖。如果賦予這些線程的優先級是固定不變的,同一批線程總是會擁有更高的優先級。為避免這個問題,可以在死鎖發生的時候設置隨機的優先級。
在多線程編程中,除了要解決數據訪問的同步與互斥之外,還需要解決的重要問題就是多線程的死鎖問題。所謂死鎖: 是指兩個或兩個以上的進程(線程)在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外部處理作用,它們都將無限等待下去。
一、死鎖原因與形成條件
死鎖形成的原因:
系統資源不足
進程(線程)推進的順序不恰當;
資源分配不當
死鎖形成的條件:
互斥條件:所謂互斥就是進程在某一時間內獨占資源。
請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
不剝奪條件:進程已獲得資源,在末使用完之前,不能強行剝奪。
循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系。
從編程經驗上來講,形成死鎖的一般原因有以下幾種:
個人使用鎖的經驗差異。
程序模塊使用鎖的差異。
工程代碼版本之間的差異。
工程代碼分支之間的差異。
修改代碼和重構代碼帶來的差異。
二、常見死鎖形成的場景
死鎖形成的常見情況有以下幾種:
2.1 忘記釋放鎖
void data_process() { EnterCriticalSection(); if(/* error happens, forget LeaveCriticalSection */) return; LeaveCriticalSection(); }
2.2 單線程重復申請鎖
void sub_func() { EnterCriticalSection(); do_something(); LeaveCriticalSection(); } void data_process() { EnterCriticalSection(); sub_func(); LeaveCriticalSection(); }
2.3 多線程多鎖申請
void data_process1() { EnterCriticalSection(&cs1); // 申請鎖的順序有依賴 EnterCriticalSection(&cs2); do_something1(); LeaveCriticalSection(&cs2); LeaveCriticalSection(&cs1); } void data_process2() { EnterCriticalSection(&cs2); // 申請鎖的順序有依賴 EnterCriticalSection(&cs1); do_something2(); LeaveCriticalSection(&cs1); LeaveCriticalSection(&cs2); }
2.4 環形鎖申請
/* 多個線程申請鎖的順序形成相互依賴的環形:
* A - B
* | |
* C - D
*/
三、死鎖的避免策略
死鎖的代價是非常大的,有時候很難檢測排查,因此需要在編程過程中盡可能的避免發生死鎖。編程中為了避免死鎖應該遵循如下策略:
在編寫多線程程序之前,首先編寫正確的程序,然后再移植到多線程。
時刻檢查自己寫的程序有沒有在跳出時忘記釋放鎖。
如果自己的模塊可能重復使用一個鎖,建議使用嵌套鎖。
對於某些鎖代碼,不要臨時重新編寫,建議使用庫里面的鎖,或者自己曾經編寫的鎖。
如果某項業務需要獲取多個鎖,必須保證鎖的按某種順序獲取,否則必定死鎖。
編寫簡單的測試用例,驗證有沒有死鎖。
編寫驗證死鎖的程序,從源頭避免死鎖。