多線程以及多進程改善了系統資源的利用率並提升了系統 的處理能力。然而,併發執行也帶來了新的問題——死鎖。所謂死鎖是指多個線程因競爭資源而形成的一種僵局(互相等待),若無外力做用,這些進程都將沒法向前推動。
下面咱們經過一些實例來講明死鎖現象。
先看生活中的一個實例,2我的一塊兒吃飯可是隻有一雙筷子,2人輪流吃(同時擁有2只筷子才能吃)。某一個時候,一個拿了左筷子,一人拿了右筷子,2我的都同時佔用一個資源,等待另外一個資源,這個時候甲在等待乙吃完並釋放它佔有的筷子,同理,乙也在等待甲吃完並釋放它佔有的筷子,這樣就陷入了一個死循環,誰也沒法繼續吃飯。。。
在計算機系統中也存在相似的狀況。例如,某計算機系統中只有一臺打印機和一臺輸入 設備,進程P1正佔用輸入設備,同時又提出使用打印機的請求,但此時打印機正被進程P2 所佔用,而P2在未釋放打印機以前,又提出請求使用正被P1佔用着的輸入設備。這樣兩個進程相互無休止地等待下去,均沒法繼續執行,此時兩個進程陷入死鎖狀態。java
一般系統中擁有的不可剝奪資源,其數量不足以知足多個進程運行的須要,使得進程在 運行過程當中,會因爭奪資源而陷入僵局,如磁帶機、打印機等。只有對不可剝奪資源的競爭 纔可能產生死鎖,對可剝奪資源的競爭是不會引發死鎖的。數據結構
進程在運行過程當中,請求和釋放資源的順序不當,也一樣會致使死鎖。例如,併發進程 P一、P2分別保持了資源R一、R2,而進程P1申請資源R2,進程P2申請資源R1時,二者都 會由於所需資源被佔用而阻塞。
信號量使用不當也會形成死鎖。進程間彼此相互等待對方發來的消息,結果也會使得這 些進程間沒法繼續向前推動。例如,進程A等待進程B發的消息,進程B又在等待進程A 發的消息,能夠看出進程A和B不是由於競爭同一資源,而是在等待對方的資源致使死鎖。多線程
產生死鎖必須同時知足如下四個條件,只要其中任一條件不成立,死鎖就不會發生。併發
直觀上看,循環等待條件彷佛和死鎖的定義同樣,其實否則。按死鎖定義構成等待環所 要求的條件更嚴,它要求Pi等待的資源必須由P(i+1)來知足,而循環等待條件則無此限制。 例如,系統中有兩臺輸出設備,P0佔有一臺,PK佔有另外一臺,且K不屬於集合{0, 1, ..., n}。
Pn等待一臺輸出設備,它能夠從P0得到,也可能從PK得到。所以,雖然Pn、P0和其餘 一些進程造成了循環等待圈,但PK不在圈內,若PK釋放了輸出設備,則可打破循環等待, 如圖2-16所示。所以循環等待只是死鎖的必要條件。
dom
資源分配圖含圈而系統又不必定有死鎖的緣由是同類資源數大於1。但若系統中每類資 源都只有一個資源,則資源分配圖含圈就變成了系統出現死鎖的充分必要條件。ide
產生死鎖的一個例子工具
* 而td1在睡眠的時候另外一個flag==0的對象(td2)線程啓動,先鎖定o2,睡眠500毫秒spa
* td1睡眠結束後須要鎖定o2才能繼續執行,而此時o2已被td2鎖定;線程
* td2睡眠結束後須要鎖定o1才能繼續執行,而此時o1已被td1鎖定;3d
* td一、td2相互等待,都須要獲得對方鎖定的資源才能繼續執行,從而死鎖。
*/
public int flag = 1;
//靜態對象是類的全部對象共享的
private static Object o1 = new Object(), o2 = new Object();
public void run() {
System.out.println("flag=" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;
//td1,td2都處於可執行狀態,但JVM線程調度先執行哪一個線程是不肯定的。
//td2的run()可能在td1的run()以前運行
new Thread(td1).start();
new Thread(td2).start();
}
}
在有些狀況下死鎖是能夠避免的。三種用於避免死鎖的技術:
當多個線程須要相同的一些鎖,可是按照不一樣的順序加鎖,死鎖就很容易發生。
若是能確保全部的線程都是按照相同的順序得到鎖,那麼死鎖就不會發生。看下面這個例子:
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.
在上面的例子中,線程2比線程1早200毫秒進行重試加鎖,所以它能夠先成功地獲取到兩個鎖。這時,線程1嘗試獲取鎖A而且處於等待狀態。當線程2結束時,線程1也能夠順利的得到這兩個鎖(除非線程2或者其它線程在線程1成功得到兩個鎖以前又得到其中的一些鎖)。
須要注意的是,因爲存在鎖的超時,因此咱們不能認爲這種場景就必定是出現了死鎖。也多是由於得到了鎖的線程(致使其它線程超時)須要很長的時間去完成它的任務。
此外,若是有很是多的線程同一時間去競爭同一批資源,就算有超時和回退機制,仍是可能會致使這些線程重複地嘗試但卻始終得不到鎖。若是隻有兩個線程,而且重試的超時時間設定爲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)之間鎖佔有和請求的關係圖。像這樣的數據結構就能夠被用來檢測死鎖。
那麼當檢測出死鎖時,這些線程該作些什麼呢?
一個可行的作法是釋放全部鎖,回退,而且等待一段隨機的時間後重試。這個和簡單的加鎖超時相似,不同的是隻有死鎖已經發生了纔回退,而不會是由於加鎖的請求超時了。雖然有回退和等待,可是若是有大量的線程競爭同一批鎖,它們仍是會重複地死鎖(編者注:緣由同超時相似,不能從根本上減輕競爭)。
一個更好的方案是給這些線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖同樣繼續保持着它們須要的鎖。若是賦予這些線程的優先級是固定不變的,同一批線程老是會擁有更高的優先級。爲避免這個問題,能夠在死鎖發生的時候設置隨機的優先級。