Java多執行緒---死鎖
技術標籤:Java
一、死鎖的定義
多執行緒以及多程序改善了系統資源的利用率並提高了系統 的處理能力。然而,併發執行也帶來了新的問題——死鎖。所謂死鎖是指多個執行緒因競爭資源而造成的一種僵局(互相等待),若無外力作用,這些程序都將無法向前推進。
二、死鎖產生的原因
1.系統資源的競爭
通常系統中擁有的不可剝奪資源,其數量不足以滿足多個程序執行的需要,使得程序在執行過程中,會因爭奪資源而陷入僵局,如磁帶機、印表機等。只有對不可剝奪資源的競爭才可能產生死鎖,對可剝奪資源的競爭是不會引起死鎖的。
2.程序推進順序非法
Java中死鎖最簡單的情況是,一個執行緒T1持有鎖L1並且申請獲得鎖L2,而另一個執行緒T2持有鎖L2並且申請獲得鎖L1,因為預設的鎖申請操作都是阻塞的,所以執行緒T1和T2永遠被阻塞了。導致了死鎖。
總而言之,產生死鎖可能性的最根本原因是:執行緒在獲得一個鎖L1的情況下再去申請另外一個鎖L2,也就是鎖L1想要包含了鎖L2,也就是說在獲得了鎖L1,並且沒有釋放鎖L1的情況下,又去申請獲得鎖L2,這個是產生死鎖的最根本原因。另一個原因是預設的鎖申請操作是阻塞的。
三、死鎖產生的必要條件
1.互斥條件:一個資源每次只能被一個執行緒使用。此時若有其他程序請求該資源,則請求程序只能等待。
2.不剝奪條件:程序所獲得的資源在未使用完畢之前,不能被其他程序強行奪走,即只能由獲得該資源的程序自己來釋放(只能是主動釋放)。
3.請求和保持條件:程序已經保持了至少一個資源,但又提出了新的資源請求,而該資源已被其他程序佔有,此時請求程序被阻塞,但對自己已獲得的資源保持不放。
4.迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係。
注:迴圈等待條件和死鎖的區別
迴圈等待條件是存在一種程序資源的迴圈等待鏈,鏈中每一個程序已獲得的資源同時被鏈中下一個程序所請求。即存在一個處於等待狀態的程序集合{Pl, P2, ..., pn},其中Pi等 待的資源被P(i+1)佔有(i=0, 1, ..., n-1),Pn等待的資源被P0佔有。
直觀上看,迴圈等待條件似乎和死鎖的定義一樣,其實不然。按死鎖定義構成等待環所 要求的條件更嚴,它要求Pi等待的資源必須由P(i+1)來滿足,而迴圈等待條件則無此限制。 例如,系統中有兩臺輸出裝置,P0佔有一臺,PK佔有另一臺,且K不屬於集合{0, 1, ..., n}。
Pn等待一臺輸出裝置,它可以從P0獲得,也可能從PK獲得。因此,雖然Pn、P0和其他 一些程序形成了迴圈等待圈,但PK不在圈內,若PK釋放了輸出裝置,則可打破迴圈等待。因此迴圈等待只是死鎖的必要條件。
總的來說,死鎖要求當前程序只能從迴圈鏈的下一個來滿足,而迴圈等待條件是可以從迴圈鏈下一個來獲取,也可以從其他途徑獲得。
四、死鎖的例項
public class DeadLockTest {
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
new Thread(){
@Override
public void run() {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " got lock1, want lock2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " got lock2");
System.out.println(Thread.currentThread().getName() + " got lock1 and lock2");
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " got lock2, want lock1");
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " got lock1");
System.out.println(Thread.currentThread().getName() + " got lock2 and lock1");
}
}
}
}).start();
}
}
結果:
Thread-0 got lock1, want lock2
Thread-1 got lock2, want lock1
五、如何避免死鎖
死鎖是由四個必要條件導致的,所以一般來說,只要破壞這四個必要條件中的一個條件,死鎖情況就應該不會發生。
- 加鎖順序(執行緒按照一定的順序加鎖)
- 加鎖時限(執行緒嘗試獲取鎖的時候加上一定的時限,超過時限則放棄對該鎖的請求,並釋放自己佔有的鎖)
- 死鎖檢測
1.加鎖順序
當多個執行緒需要相同的一些鎖,但是按照不同的順序加鎖,死鎖就很容易發生。如果能確保所有的執行緒都是按照相同的順序獲得鎖,那麼死鎖就不會發生。
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加了鎖。
按照順序加鎖是一種有效的死鎖預防機制。但是,這種方式需要你事先知道所有可能會用到的鎖(並對這些鎖做適當的排序),但總有些時候是無法預知的。
2.加鎖時限
另外一個可以避免死鎖的方法是在嘗試獲取鎖的時候加一個超時時間,這也就意味著在嘗試獲取鎖的過程中若超過了這個時限該執行緒則放棄對該鎖請求。若一個執行緒沒有在給定的時限內成功獲得所有需要的鎖,則會進行回退並釋放所有已經獲得的鎖,然後等待一段隨機的時間再重試。這段隨機的等待時間讓其它執行緒有機會嘗試獲取相同的這些鎖,並且讓該應用在沒有獲得鎖的時候可以繼續執行(加鎖超時後可以先繼續執行乾點其它事情,再回頭來重複之前加鎖的邏輯)。
這種機制存在一個問題,在Java中不能對synchronized同步塊設定超時時間。你需要建立一個自定義鎖,或使用Java5中java.util.concurrent包下的工具。
3.死鎖檢測
死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖並且鎖超時也不可行的場景。
每當一個執行緒獲得了鎖,會線上程和鎖相關的資料結構中(map、graph等等)將其記下。除此之外,每當有執行緒請求鎖,也需要記錄在這個資料結構中。當一個執行緒請求鎖失敗時,這個執行緒可以遍歷鎖的關係圖看看是否有死鎖發生。