1. 程式人生 > 實用技巧 >如何避免死鎖

如何避免死鎖

避免死鎖

在有些情況下死鎖是可以避免的。本文將展示三種用於避免死鎖的技術:

  1. 加鎖順序
  2. 加鎖時限
  3. 死鎖檢測

加鎖順序

當多個執行緒需要相同的一些鎖,但是按照不同的順序加鎖,死鎖就很容易發生。

如果能確保所有的執行緒都是按照相同的順序獲得鎖,那麼死鎖就不會發生。看下面這個例子:

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)之間鎖佔有和請求的關係圖。像這樣的資料結構就可以被用來檢測死鎖。

那麼當檢測出死鎖時,這些執行緒該做些什麼呢?

一個可行的做法是釋放所有鎖,回退,並且等待一段隨機的時間後重試。這個和簡單的加鎖超時類似,不一樣的是隻有死鎖已經發生了才回退,而不會是因為加鎖的請求超時了。雖然有回退和等待,但是如果有大量的執行緒競爭同一批鎖,它們還是會重複地死鎖(編者注:原因同超時類似,不能從根本上減輕競爭)。

一個更好的方案是給這些執行緒設定優先順序,讓一個(或幾個)執行緒回退,剩下的執行緒就像沒發生死鎖一樣繼續保持著它們需要的鎖。如果賦予這些執行緒的優先順序是固定不變的,同一批執行緒總是會擁有更高的優先順序。為避免這個問題,可以在死鎖發生的時候設定隨機的優先順序。