一、什么是死鎖
多線程以及多進程改善了系統資源的利用率並提高了系統的處理能力。然而,並發執行也帶來了新的問題——死鎖。
死鎖是指兩個或兩個以上的進程(線程)在運行過程中因爭奪資源而造成的一種僵局(Deadly-Embrace) ) ,若無外力作用,這些進程(線程)都將無法向前推進。
下面我們通過一些實例來說明死鎖現象。
先看生活中的一個實例,2個人一起吃飯但是只有一雙筷子,2人輪流吃(同時擁有2只筷子才能吃)。某一個時候,一個拿了左筷子,一人拿了右筷子,2個人都同時占用一個資源,等待另一個資源,這個時候甲在等待乙吃完並釋放它占有的筷子,同理,乙也在等待甲吃完並釋放它占有的筷子,這樣就陷入了一個死循環,誰也無法繼續吃飯。。。
在計算機系統中也存在類似的情況。例如,某計算機系統中只有一台打印機和一台輸入設備,進程P1正占用輸入設備,同時又提出使用打印機的請求,但此時打印機正被進程P2 所占用,而P2在未釋放打印機之前,又提出請求使用正被P1占用着的輸入設備。這樣兩個進程相互無休止地等待下去,均無法繼續執行,此時兩個進程陷入死鎖狀態。
關於死鎖的一些結論:
-
- 參與死鎖的進程數至少為兩個
- 參與死鎖的所有進程均等待資源
- 參與死鎖的進程至少有兩個已經占有資源
- 死鎖進程是系統中當前進程集合的一個子集
- 死鎖會浪費大量系統資源,甚至導致系統崩潰。
二、死鎖與飢餓
飢餓(Starvation)指一個進程一直得不到資源。
死鎖和飢餓都是由於進程競爭資源而引起的。飢餓一般不占有資源,死鎖進程一定占有資源。
三、資源的類型
3.1 可重用資源和消耗性資源
3.1.1 可重用資源(永久性資源)
可被多個進程多次使用,如所有硬件。
只能分配給一個進程使用,不允許多個進程共享。
進程在對可重用資源的使用時,須按照請求資源、使用資源、釋放資源這樣的順序。
系統中每一類可重用資源中的單元數目是相對固定的,進程在運行期間,既不能創建,也不能刪除。
3.1.2 消耗性資源(臨時性資源)
又稱臨時性資源,是由進程在運行期間動態的創建和消耗的。
消耗性資源在進程運行期間是可以不斷變化的,有時可能為0。
進程在運行過程中,可以不斷地創造可消耗性資源的單元,將它們放入該資源類的緩沖區中,以增加該資源類的單元數目。
進程在運行過程中,可以請求若干個可消耗性資源單元,用於進程自己消耗,不再將它們返回給該資源類中。
可消耗資源通常是由生產者進程創建,由消費者進程消耗。最典型的可消耗資源是用於進程間通信的消息。
3.2 可搶占資源和不可搶占資源
3.2.1 可搶占資源
可搶占資源指某進程在獲得這類資源后,該資源可以再被其他進程或系統搶占。對於這類資源是不會引起死鎖的。
CPU 和主存均屬於可搶占性資源。
3.2.2 不可搶占資源
一旦系統把某資源分配給該進程后,就不能將它強行收回,只能在進程用完后自行釋放。
磁帶機、打印機等屬於不可搶占性資源。
四、死鎖產生的原因
- 競爭不可搶占資源引起死鎖
通常系統中擁有的不可搶占資源,其數量不足以滿足多個進程運行的需要,使得進程在運行過程中,會因爭奪資源而陷入僵局,如磁帶機、打印機等。只有對不可搶占資源的競爭 才可能產生死鎖,對可搶占資源的競爭是不會引起死鎖的。
- 競爭可消耗資源引起死鎖
- 進程推進順序不當引起死鎖
進程在運行過程中,請求和釋放資源的順序不當,也同樣會導致死鎖。例如,並發進程 P1、P2分別保持了資源R1、R2,而進程P1申請資源R2,進程P2申請資源R1時,兩者都會因為所需資源被占用而阻塞。
信號量使用不當也會造成死鎖。進程間彼此相互等待對方發來的消息,結果也會使得這些進程間無法繼續向前推進。例如,進程A等待進程B發的消息,進程B又在等待進程A 發的消息,可以看出進程A和B不是因為競爭同一資源,而是在等待對方的資源導致死鎖。
4.1 競爭不可搶占資源引起死鎖
如:共享文件時引起死鎖
系統中擁有兩個進程P1和P2,它們都准備寫兩個文件F1和F2。而這兩者都屬於可重用和不可搶占性資源。如果進程P1在打開F1的同時,P2進程打開F2文件,當P1想打開F2時由於F2已結被占用而阻塞,當P2想打開1時由於F1已結被占用而阻塞,此時就會無線等待下去,形成死鎖。
4.2 競爭可消耗資源引起死鎖
如:進程通信時引起死鎖
系統中擁有三個進程P1、P2和P3,m1、m2、m3是3個可消耗資源。進程P1一方面產生消息m1,將其發送給P2,另一方面要從P3接收消息m3。而進程P2一方面產生消息m2,將其發送給P3,另一方面要從P1接收消息m1。類似的,進程P3一方面產生消息m3,將其發送給P1,另一方面要從P2接收消息m2。
如果三個進程都先發送自己產生的消息后接收別人發來的消息,則可以順利的運行下去不會產生死鎖,但要是三個進程都先接收別人的消息而不產生消息則會永遠等待下去,產生死鎖。
4.3 進程推進順序不當引起死鎖
上圖中,如果按曲線1的順序推進,兩個進程可順利完成;如果按曲線2的順序推進,兩個進程可順利完成;如果按曲線3的順序推進,兩個進程可順利完成;如果按曲線4的順序推進,兩個進程將進入不安全區D中,此時P1保持了資源R1,P2保持了資源R2,系統處於不安全狀態,如果繼續向前推進,則可能產生死鎖。
五、產生死鎖的四個必要條件
5.1 互斥條件
進程要求對所分配的資源(如打印機)進行排他性控制,即在一段時間內某資源僅為一個進程所占有。此時若有其他進程請求該資源,則請求進程只能等待。
5.2 不可剝奪條件
進程所獲得的資源在未使用完畢之前,不能被其他進程強行奪走,即只能由獲得該資源的進程自己來釋放(只能是主動釋放)。
5.3 請求與保持條件
進程已經保持了至少一個資源,但又提出了新的資源請求,而該資源已被其他進程占有,此時請求進程被阻塞,但對自己已獲得的資源保持不放。
5.4 循環等待條件
存在一種進程資源的循環等待鏈,鏈中每一個進程已獲得的資源同時被 鏈中下一個進程所請求。即存在一個處於等待狀態的進程集合{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所示。因此循環等待只是死鎖的必要條件。
資源分配圖含圈而系統又不一定有死鎖的原因是同類資源數大於1。但若系統中每類資 源都只有一個資源,則資源分配圖含圈就變成了系統出現死鎖的充分必要條件。
以上這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。
產生死鎖的一個例子:
1 /** 2 * 一個簡單的死鎖類 3 * 當DeadLock類的對象flag==1時(td1),先鎖定o1,睡眠500毫秒 4 * 而td1在睡眠的時候另一個flag==0的對象(td2)線程啟動,先鎖定o2,睡眠500毫秒 5 * td1睡眠結束后需要鎖定o2才能繼續執行,而此時o2已被td2鎖定; 6 * td2睡眠結束后需要鎖定o1才能繼續執行,而此時o1已被td1鎖定; 7 * td1、td2相互等待,都需要得到對方鎖定的資源才能繼續執行,從而死鎖。 8 */
9 public class DeadLock implements Runnable { 10 public int flag = 1; 11 //靜態對象是類的所有對象共享的
12 private static Object o1 = new Object(), o2 = new Object(); 13 @Override 14 public void run() { 15 System.out.println("flag=" + flag); 16 if (flag == 1) { 17 synchronized (o1) { 18 try { 19 Thread.sleep(500); 20 } catch (Exception e) { 21 e.printStackTrace(); 22 } 23 synchronized (o2) { 24 System.out.println("1"); 25 } 26 } 27 } 28 if (flag == 0) { 29 synchronized (o2) { 30 try { 31 Thread.sleep(500); 32 } catch (Exception e) { 33 e.printStackTrace(); 34 } 35 synchronized (o1) { 36 System.out.println("0"); 37 } 38 } 39 } 40 } 41
42 public static void main(String[] args) { 43 DeadLock td1 = new DeadLock(); 44 DeadLock td2 = new DeadLock(); 45 td1.flag = 1; 46 td2.flag = 0; 47 //td1,td2都處於可執行狀態,但JVM線程調度先執行哪個線程是不確定的。 48 //td2的run()可能在td1的run()之前運行
49 new Thread(td1).start(); 50 new Thread(td2).start(); 51 } 52 }
六、處理死鎖的方法
- 預防死鎖:通過設置某些限制條件,去破壞產生死鎖的四個必要條件中的一個或幾個條件,來防止死鎖的發生。
- 避免死鎖:在資源的動態分配過程中,用某種方法去防止系統進入不安全狀態,從而避免死鎖的發生。
- 檢測死鎖:允許系統在運行過程中發生死鎖,但可設置檢測機構及時檢測死鎖的發生,並采取適當措施加以清除。
- 解除死鎖:當檢測出死鎖后,便采取適當措施將進程從死鎖狀態中解脫出來。
6.1 預防死鎖
- 破壞“互斥”條件
就是在系統里取消互斥。若資源不被一個進程獨占使用,那么死鎖是肯定不會發生的。但一般來說在所列的四個條件中,“互斥”條件是無法破壞的。因此,在死鎖預防里主要是破壞其他幾個必要條件,而不去涉及破壞“互斥”條件。
注意:互斥條件不能被破壞,否則會造成結果的不可再現性。
- 破壞“請求與保持”條件
破壞“占有並等待”條件,就是在系統中不允許進程在已獲得某種資源的情況下,申請其他資源。即要想出一個辦法,阻止進程在持有資源的同時申請其他資源。
方法一:創建進程時,要求它申請所需的全部資源,系統或滿足其所有要求,或什么也不給它。這是所謂的 “ 一次性分配”方案。
方法二:要求每個進程提出新的資源申請前,釋放它所占有的資源。這樣,一個進程在需要資源S時,須先把它先前占有的資源R釋放掉,然后才能提出對S的申請,即使它可能很快又要用到資源R。
- 破壞“不可搶占”條件
破壞“不可搶占”條件就是允許對資源實行搶奪。
方法一:如果占有某些資源的一個進程進行進一步資源請求被拒絕,則該進程必須釋放它最初占有的資源,如果有必要,可再次請求這些資源和另外的資源。
方法二:如果一個進程請求當前被另一個進程占有的一個資源,則操作系統可以搶占另一個進程,要求它釋放資源。只有在任意兩個進程的優先級都不相同的條件下,方法二才能預防死鎖。
- 破壞“循環等待”條件
破壞“循環等待”條件的一種方法,是將系統中的所有資源統一編號,進程可在任何時刻提出資源申請,但所有申請必須按照資源的編號順序(升序)提出。這樣做就能保證系統不出現死鎖。
6.2 避免死鎖
理解了死鎖的原因,尤其是產生死鎖的四個必要條件,就可以最大可能地預防、避免和解除死鎖。所以,在系統設計、進程調度等方面注意如何讓這四個必要條件不成立,如何確定資源的合理分配算法,避免進程永久占據系統資源。此外,也要防止進程在處於等待狀態的情況下占用資源。因此,對資源的分配要給予合理的規划。
6.2.1 預防死鎖和避免死鎖的區別
6.2.2 常用避免死鎖的方法
6.2.2.1 有序資源分配法
這種算法要求資源按某種規則系統中的所有資源統一編號(例如打印機為1、磁帶機為2、磁盤為3、等等),申請時必須以上升的次序。系統要求申請進程:
1、對它所必須使用的而且屬於同一類的所有資源,必須一次申請完;
2、在申請不同類資源時,必須按各類設備的編號依次申請。例如:進程PA,使用資源的順序是R1,R2; 進程PB,使用資源的順序是R2,R1;若采用動態分配有可能形成環路條件,造成死鎖。
采用有序資源分配法:R1的編號為1,R2的編號為2;
PA:申請次序應是:R1,R2
PB:申請次序應是:R1,R2
這樣就破壞了環路條件,避免了死鎖的發生。
6.2.2.2 銀行家算法
6.2.3 常用避免死鎖的技術
- 加鎖順序(線程按照一定的順序加鎖)
- 加鎖時限(線程嘗試獲取鎖的時候加上一定的時限,超過時限則放棄對該鎖的請求,並釋放自己占有的鎖)
- 死鎖檢測
6.2.3.1 加鎖順序
當多個線程需要相同的一些鎖,但是按照不同的順序加鎖,死鎖就很容易發生。
如果能確保所有的線程都是按照相同的順序獲得鎖,那么死鎖就不會發生。看下面這個例子:
1 Thread 1: 2 lock A 3 lock B 4 Thread 2: 5 wait for A 6 lock C (when A locked) 7 Thread 3: 8 wait for A 9 wait for B 10 wait for C
如果一個線程(比如線程3)需要一些鎖,那么它必須按照確定的順序獲取鎖。它只有獲得了從順序上排在前面的鎖之后,才能獲取后面的鎖。
例如,線程2和線程3只有在獲取了鎖A之后才能嘗試獲取鎖C(譯者注:獲取鎖A是獲取鎖C的必要條件)。因為線程1已經擁有了鎖A,所以線程2和3需要一直等到鎖A被釋放。然后在它們嘗試對B或C加鎖之前,必須成功地對A加了鎖。
按照順序加鎖是一種有效的死鎖預防機制。但是,這種方式需要你事先知道所有可能會用到的鎖(譯者注:並對這些鎖做適當的排序),但總有些時候是無法預知的。
6.2.3.2 加鎖時限
另外一個可以避免死鎖的方法是在嘗試獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程中若超過了這個時限該線程則放棄對該鎖請求。若一個線程沒有在給定的時限內成功獲得所有需要的鎖,則會進行回退並釋放所有已經獲得的鎖,然后等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,並且讓該應用在沒有獲得鎖的時候可以繼續運行(譯者注:加鎖超時后可以先繼續運行干點其它事情,再回頭來重復之前加鎖的邏輯)。
以下是一個例子,展示了兩個線程以不同的順序嘗試獲取相同的兩個鎖,在發生超時后回退並重試的場景:
1 Thread 1 locks A 2 Thread 2 locks B 3 Thread 1 attempts to lock B but is blocked 4 Thread 2 attempts to lock A but is blocked 5 Thread 1’s lock attempt on B times out
6 Thread 1 backs up and releases A as well 7 Thread 1 waits randomly (e.g. 257 millis) before retrying. 8 Thread 2’s lock attempt on A times out
9 Thread 2 backs up and releases B as well 10 Thread 2 waits randomly (e.g. 43 millis) before retrying.
在上面的例子中,線程2比線程1早200毫秒進行重試加鎖,因此它可以先成功地獲取到兩個鎖。這時,線程1嘗試獲取鎖A並且處於等待狀態。當線程2結束時,線程1也可以順利的獲得這
兩個鎖(除非線程2或者其它線程在線程1成功獲得兩個鎖之前又獲得其中的一些鎖)。
需要注意的是,由於存在鎖的超時,所以我們不能認為這種場景就一定是出現了死鎖。也可能是因為獲得了鎖的線程(導致其它線程超時)需要很長的時間去完成它的任務。
此外,如果有非常多的線程同一時間去競爭同一批資源,就算有超時和回退機制,還是可能會導致這些線程重復地嘗試但卻始終得不到鎖。如果只有兩個線程,並且重試的超時時間設定
為0到500毫秒之間,這種現象可能不會發生,但是如果是10個或20個線程情況就不同了。因為這些線程等待相等的重試時間的概率就高的多(或者非常接近以至於會出現問題)。
(譯者注:超時和重試機制是為了避免在同一時間出現的競爭,但是當線程很多時,其中兩個或多個線程的超時時間一樣或者接近的可能性就會很大,因此就算出現競爭而導致超時后,由於超
時時間一樣,它們又會同時開始重試,導致新一輪的競爭,帶來了新的問題。)
這種機制存在一個問題,在Java中不能對synchronized同步塊設置超時時間。你需要創建一個自定義鎖,或使用Java5中java.util.concurrent包下的工具。寫一個自定義鎖類不復雜,但超
出了本文的內容。后續的Java並發系列會涵蓋自定義鎖的內容。
6.2.3.3 死鎖檢測
死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖並且鎖超時也不可行的場景。
每當一個線程獲得了鎖,會在線程和鎖相關的數據結構中(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)之間鎖占有和請求的關系圖。像這樣的數據結構就可以被用來檢測死鎖。
6.3 檢測死鎖
一般來說,由於操作系統有並發,共享以及隨機性等特點,通過預防和避免的手段達到排除死鎖的目的是很困難的。這需要較大的系統開銷,而且不能充分利用資源。為此,一種簡便的
方法是系統為進程分配資源時,不采取任何限制性措施,但是提供了檢測和解脫死鎖的手段:能發現死鎖並從死鎖狀態中恢復出來。因此,在實際的操作系統中往往采用死鎖的檢測與恢復方
法來排除死鎖。
死鎖檢測與恢復是指系統設有專門的機構,當死鎖發生時,該機構能夠檢測到死鎖發生的位置和原因,並能通過外力破壞死鎖發生的必要條件,從而使得並發進程從死鎖狀態中恢復出來。
這時進程P1占有資源R1而申請資源R2,進程P2占有資源R2而申請資源R1,按循環等待條件,進程和資源形成了環路,所以系統是死鎖狀態。進程P1,P2是參與死鎖的進程。
下面我們再來看一看死鎖檢測算法。算法使用的數據結構是如下這些:
占有矩陣A:n*m階,其中n表示並發進程的個數,m表示系統的各類資源的個數,這個矩陣記錄了每一個進程當前占有各個資源類中資源的個數。
申請矩陣R:n*m階,其中n表示並發進程的個數,m表示系統的各類資源的個數,這個矩陣記錄了每一個進程當前要完成工作需要申請的各個資源類中資源的個數。
空閑向量T:記錄當前m個資源類中空閑資源的個數。
完成向量F:布爾型向量值為真(true)或假(false),記錄當前n個並發進程能否進行完。為真即能進行完,為假則不能進行完。
臨時向量W:開始時W:=T。
算法步驟:
(1)W:=T,
對於所有的i=1,2,…,n,
如果A[i]=0,則F[i]:=true;否則,F[i]:=false
(2)找滿足下面條件的下標i:
F[i]:=false並且R[i]〈=W
如果不存在滿足上面的條件i,則轉到步驟(4)。
(3)W:=W+A[i]
F[i]:=true
轉到步驟(2)
(4)如果存在i,F[i]:=false,則系統處於死鎖狀態,且Pi進程參與了死鎖。
什么時候進行死鎖的檢測取決於死鎖發生的頻率。如果死鎖發生的頻率高,那么死鎖檢測的頻率也要相應提高,這樣一方面可以提高系統資源的利用率,一方面可以避免更多的進程卷入死鎖。如果進程申請資源不能滿足就立刻進行檢測,那么每當死鎖形成時即能被發現,這和死鎖避免的算法相近,只是系統的開銷較大。為了減小死鎖檢測帶來的系統開銷,一般采取每隔一段時間進行一次死鎖檢測,或者在CPU的利用率降低到某一數值時,進行死鎖的檢測。
6.4 解除死鎖
一旦檢測出死鎖,就應立即釆取相應的措施,以解除死鎖。
死鎖解除的主要方法有:
1) 資源剝奪法。掛起某些死鎖進程,並搶占它的資源,將這些資源分配給其他的死鎖進程。但應防止被掛起的進程長時間得不到資源,而處於資源匱乏的狀態。
2) 撤銷進程法。強制撤銷部分、甚至全部死鎖進程並剝奪這些進程的資源。撤銷的原則可以按進程優先級和撤銷進程代價的高低進行。
3) 進程回退法。讓一(多)個進程回退到足以回避死鎖的地步,進程回退時自願釋放資源而不是被剝奪。要求系統保持進程的歷史信息,設置還原點。、
七、轉載於
https://blog.csdn.net/wljliujuan/article/details/79614019