在有些情況下死鎖是可以避免的,。三種用于避免死鎖的技術(shù):
- 加鎖順序(線程按照一定的順序加鎖)
- 加鎖時限(線程嘗試獲取鎖的時候加上一定的時限,,超過時限則放棄對該鎖的請求,,并釋放自己占有的鎖)
- 死鎖檢測
加鎖順序
當(dāng)多個線程需要相同的一些鎖,但是按照不同的順序加鎖,,死鎖就很容易發(fā)生,。
如果能確保所有的線程都是按照相同的順序獲得鎖,那么死鎖就不會發(fā)生,??聪旅孢@個例子:
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已經(jīng)擁有了鎖A,所以線程2和3需要一直等到鎖A被釋放,。然后在它們嘗試對B或C加鎖之前,,必須成功地對A加了鎖。
按照順序加鎖是一種有效的死鎖預(yù)防機制,。但是,,這種方式需要你事先知道所有可能會用到的鎖(譯者注:并對這些鎖做適當(dāng)?shù)呐判?,,但總有些時候是無法預(yù)知的,。
加鎖時限
另外一個可以避免死鎖的方法是在嘗試獲取鎖的時候加一個超時時間,,這也就意味著在嘗試獲取鎖的過程中若超過了這個時限該線程則放棄對該鎖請求,。若一個線程沒有在給定的時限內(nèi)成功獲得所有需要的鎖,則會進行回退并釋放所有已經(jīng)獲得的鎖,,然后等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,,并且讓該應(yīng)用在沒有獲得鎖的時候可以繼續(xù)運行(譯者注:加鎖超時后可以先繼續(xù)運行干點其它事情,,再回頭來重復(fù)之前加鎖的邏輯),。
以下是一個例子,,展示了兩個線程以不同的順序嘗試獲取相同的兩個鎖,,在發(fā)生超時后回退并重試的場景:
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并且處于等待狀態(tài),。當(dāng)線程2結(jié)束時,線程1也可以順利的獲得這兩個鎖(除非線程2或者其它線程在線程1成功獲得兩個鎖之前又獲得其中的一些鎖),。
需要注意的是,,由于存在鎖的超時,所以我們不能認為這種場景就一定是出現(xiàn)了死鎖,。也可能是因為獲得了鎖的線程(導(dǎo)致其它線程超時)需要很長的時間去完成它的任務(wù),。
此外,如果有非常多的線程同一時間去競爭同一批資源,,就算有超時和回退機制,,還是可能會導(dǎo)致這些線程重復(fù)地嘗試但卻始終得不到鎖。如果只有兩個線程,,并且重試的超時時間設(shè)定為0到500毫秒之間,,這種現(xiàn)象可能不會發(fā)生,但是如果是10個或20個線程情況就不同了,。因為這些線程等待相等的重試時間的概率就高的多(或者非常接近以至于會出現(xiàn)問題),。
(譯者注:超時和重試機制是為了避免在同一時間出現(xiàn)的競爭,但是當(dāng)線程很多時,,其中兩個或多個線程的超時時間一樣或者接近的可能性就會很大,,因此就算出現(xiàn)競爭而導(dǎo)致超時后,由于超時時間一樣,,它們又會同時開始重試,,導(dǎo)致新一輪的競爭,帶來了新的問題,。)
這種機制存在一個問題,,在Java中不能對synchronized同步塊設(shè)置超時時間。你需要創(chuàng)建一個自定義鎖,,或使用Java5中java.util.concurrent包下的工具,。寫一個自定義鎖類不復(fù)雜,但超出了本文的內(nèi)容,。后續(xù)的Java并發(fā)系列會涵蓋自定義鎖的內(nèi)容,。
死鎖檢測
死鎖檢測是一個更好的死鎖預(yù)防機制,它主要是針對那些不可能實現(xiàn)按序加鎖并且鎖超時也不可行的場景,。
每當(dāng)一個線程獲得了鎖,,會在線程和鎖相關(guān)的數(shù)據(jù)結(jié)構(gòu)中(map、graph等等)將其記下,。除此之外,,每當(dāng)有線程請求鎖,,也需要記錄在這個數(shù)據(jù)結(jié)構(gòu)中。
當(dāng)一個線程請求鎖失敗時,,這個線程可以遍歷鎖的關(guān)系圖看看是否有死鎖發(fā)生,。例如,線程A請求鎖7,,但是鎖7這個時候被線程B持有,,這時線程A就可以檢查一下線程B是否已經(jīng)請求了線程A當(dāng)前所持有的鎖。如果線程B確實有這樣的請求,,那么就是發(fā)生了死鎖(線程A擁有鎖1,,請求鎖7;線程B擁有鎖7,,請求鎖1),。
當(dāng)然,死鎖一般要比兩個線程互相持有對方的鎖這種情況要復(fù)雜的多,。線程A等待線程B,,線程B等待線程C,線程C等待線程D,,線程D又在等待線程A,。線程A為了檢測死鎖,它需要遞進地檢測所有被B請求的鎖,。從線程B所請求的鎖開始,,線程A找到了線程C,然后又找到了線程D,,發(fā)現(xiàn)線程D請求的鎖被線程A自己持有著,。這是它就知道發(fā)生了死鎖,。
下面是一幅關(guān)于四個線程(A,B,C和D)之間鎖占有和請求的關(guān)系圖,。像這樣的數(shù)據(jù)結(jié)構(gòu)就可以被用來檢測死鎖。
那么當(dāng)檢測出死鎖時,,這些線程該做些什么呢,?
一個可行的做法是釋放所有鎖,回退,,并且等待一段隨機的時間后重試,。這個和簡單的加鎖超時類似,不一樣的是只有死鎖已經(jīng)發(fā)生了才回退,,而不會是因為加鎖的請求超時了,。雖然有回退和等待,但是如果有大量的線程競爭同一批鎖,,它們還是會重復(fù)地死鎖(編者注:原因同超時類似,,不能從根本上減輕競爭),。
一個更好的方案是給這些線程設(shè)置優(yōu)先級,讓一個(或幾個)線程回退,,剩下的線程就像沒發(fā)生死鎖一樣繼續(xù)保持著它們需要的鎖,。如果賦予這些線程的優(yōu)先級是固定不變的,同一批線程總是會擁有更高的優(yōu)先級,。為避免這個問題,,可以在死鎖發(fā)生的時候設(shè)置隨機的優(yōu)先級。
一,、死鎖的定義
多線程以及多進程改善了系統(tǒng)資源的利用率并提高了系統(tǒng) 的處理能力,。然而,并發(fā)執(zhí)行也帶來了新的問題——死鎖,。所謂死鎖是指多個線程因競爭資源而造成的一種僵局(互相等待),,若無外力作用,這些進程都將無法向前推進,。
下面我們通過一些實例來說明死鎖現(xiàn)象,。
先看生活中的一個實例,2個人一起吃飯但是只有一雙筷子,,2人輪流吃(同時擁有2只筷子才能吃),。某一個時候,一個拿了左筷子,,一人拿了右筷子,,2個人都同時占用一個資源,等待另一個資源,,這個時候甲在等待乙吃完并釋放它占有的筷子,,同理,乙也在等待甲吃完并釋放它占有的筷子,,這樣就陷入了一個死循環(huán),,誰也無法繼續(xù)吃飯。,。,。
在計算機系統(tǒng)中也存在類似的情況。例如,,某計算機系統(tǒng)中只有一臺打印機和一臺輸入 設(shè)備,,進程P1正占用輸入設(shè)備,同時又提出使用打印機的請求,,但此時打印機正被進程P2 所占用,,而P2在未釋放打印機之前,又提出請求使用正被P1占用著的輸入設(shè)備,。這樣兩個進程相互無休止地等待下去,,均無法繼續(xù)執(zhí)行,,此時兩個進程陷入死鎖狀態(tài)。
二,、死鎖產(chǎn)生的原因
1) 系統(tǒng)資源的競爭
通常系統(tǒng)中擁有的不可剝奪資源,,其數(shù)量不足以滿足多個進程運行的需要,使得進程在 運行過程中,,會因爭奪資源而陷入僵局,,如磁帶機、打印機等,。只有對不可剝奪資源的競爭 才可能產(chǎn)生死鎖,,對可剝奪資源的競爭是不會引起死鎖的。
2) 進程推進順序非法
進程在運行過程中,,請求和釋放資源的順序不當(dāng),,也同樣會導(dǎo)致死鎖。例如,,并發(fā)進程 P1,、P2分別保持了資源R1、R2,,而進程P1申請資源R2,,進程P2申請資源R1時,兩者都 會因為所需資源被占用而阻塞,。
信號量使用不當(dāng)也會造成死鎖,。進程間彼此相互等待對方發(fā)來的消息,結(jié)果也會使得這 些進程間無法繼續(xù)向前推進,。例如,,進程A等待進程B發(fā)的消息,進程B又在等待進程A 發(fā)的消息,,可以看出進程A和B不是因為競爭同一資源,,而是在等待對方的資源導(dǎo)致死鎖。
3) 死鎖產(chǎn)生的必要條件
產(chǎn)生死鎖必須同時滿足以下四個條件,,只要其中任一條件不成立,,死鎖就不會發(fā)生,。
- 互斥條件:進程要求對所分配的資源(如打印機)進行排他性控制,,即在一段時間內(nèi)某 資源僅為一個進程所占有。此時若有其他進程請求該資源,,則請求進程只能等待,。
- 不剝奪條件:進程所獲得的資源在未使用完畢之前,不能被其他進程強行奪走,,即只能 由獲得該資源的進程自己來釋放(只能是主動釋放),。
- 請求和保持條件:進程已經(jīng)保持了至少一個資源,,但又提出了新的資源請求,而該資源 已被其他進程占有,,此時請求進程被阻塞,,但對自己已獲得的資源保持不放。
- 循環(huán)等待條件:存在一種進程資源的循環(huán)等待鏈,,鏈中每一個進程已獲得的資源同時被 鏈中下一個進程所請求,。即存在一個處于等待狀態(tài)的進程集合{Pl, P2, ..., pn},其中Pi等 待的資源被P(i+1)占有(i=0, 1, ..., n-1),,Pn等待的資源被P0占有,,如圖2-15所示。
直觀上看,,循環(huán)等待條件似乎和死鎖的定義一樣,,其實不然。按死鎖定義構(gòu)成等待環(huán)所 要求的條件更嚴,,它要求Pi等待的資源必須由P(i+1)來滿足,,而循環(huán)等待條件則無此限制。 例如,,系統(tǒng)中有兩臺輸出設(shè)備,,P0占有一臺,PK占有另一臺,,且K不屬于集合{0, 1, ..., n},。
Pn等待一臺輸出設(shè)備,它可以從P0獲得,,也可能從PK獲得,。因此,雖然Pn,、P0和其他 一些進程形成了循環(huán)等待圈,,但PK不在圈內(nèi),若PK釋放了輸出設(shè)備,,則可打破循環(huán)等待, 如圖2-16所示,。因此循環(huán)等待只是死鎖的必要條件。
資源分配圖含圈而系統(tǒng)又不一定有死鎖的原因是同類資源數(shù)大于1,。但若系統(tǒng)中每類資 源都只有一個資源,,則資源分配圖含圈就變成了系統(tǒng)出現(xiàn)死鎖的充分必要條件。
產(chǎn)生死鎖的一個例子
-
-
-
-
-
-
-
-
- public class DeadLock implements Runnable {
- public int flag = 1;
-
- private static Object o1 = new Object(), o2 = new Object();
- @Override
- 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;
-
-
- new Thread(td1).start();
- new Thread(td2).start();
-
- }
- }