java 多執行緒 死鎖 哲學家就餐問題
現在你理解了,一個物件可以有synchronized方法或其他形式的加鎖機制來防止別的任務在互斥還沒有釋放的時候就訪問這個物件。你已經學習過,任務可以變成阻塞狀態,所以就可能出現兩種情況:某個惹我怒在等待另一個任務,而後者又等待別的任務,這樣一直下去,直到這個鏈條上的任務又在等待第一個任務釋放鎖。這得到了一個任務之間相互等待的連續迴圈,沒有哪個執行緒恩給你繼續。這被稱之為 死鎖 。
如果你執行一個程式,而它馬上就死鎖了,你可以立即跟蹤下去。真正的問題在於,程式可能看起來工作良好,但是具有潛在的死鎖危險。這時,死鎖可能發生,而事先卻沒有任何徵兆,所以缺陷會潛伏在你的程式裡,直到客戶發現它出乎意料地發生(以一種幾乎肯定是很難重現的方式發生)。因此,在編寫併發程式的時候,進行仔細的程式設計以防止死鎖是關鍵部分。
由Edsger Dijkstra提出的 哲學家就餐 問題是一個經典的死鎖例證。該問題的基本描述中是指定五個哲學家(不過這裡的例子中將允許任意數目)。這些哲學家將花費時間思考,花部分時間就餐。當他們思考的時候,就不需要任何共享資源;但當他們就餐時,將使用有限數量的餐具。在問題的原始描述中,餐具是叉子。要吃到桌子中央盤子裡的義大利麵條需要用兩把叉子,不過把餐具看成是筷子更合理;很明顯,哲學家要就餐就需要兩根筷子。
問題中引入的難點是:作為哲學家,他們很窮,所以他們只能買五根筷子(更一般地講,筷子和哲學家的數量相同)。他們圍坐在一個桌子周圍,每人之間放一根筷子。當一個哲學家要就餐的時候,這個哲學家必須同時得到左邊和右邊的筷子。如果一個哲學家左邊或右邊已經有人在使用筷子了,那麼這個哲學家就必須等待,直到可得到必須的筷子。
/** * 筷子 * * 任何兩個哲學家(Philosopher)都不能成功take()同一根筷子。另外,如果一根筷子(Chopstick)已經被某個哲學家(Philosopher)獲得, * 那麼另一個Philosopher可以wait(),直到這根Chopstick的當前持有者呼叫drop()使其可用為止。 * * 當一個Philosopher任務呼叫take()時,這個philosopher將等待,直至taken標誌為false(直至當前持有Chopstick的Philosopher釋放它)。 * 然後這個任務會將taken標誌設定為true,以表示現在由新的Philosopher持有這根Chopstick。當這個Philosopher使用完這根Chopstick時, * 它會呼叫drop()來修改標誌的狀態,並notifyAll()所有其他的Philosopher,這些Philosopher中有些可能就在wait()這根Chopstick。 * * @create @author Henry @date 2016-12-26 * */ public class Chopstick { private boolean taken = false; public synchronized void take() throws InterruptedException { while (taken) wait(); taken = true; } public synchronized void drop() { taken = false; notifyAll(); } }
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* 哲學家
*
* 在Philosopher,run()中,每個Philosopher只是不斷地思考和吃飯。如果PonderFactor不為0,則pause()方法會休眠(sleep())
* 一段隨機的時間。通過使用這種方式,你將看到Philosopher會在思考上花掉一段隨機化的時間,然後嘗試著獲取(take())右邊和左邊的
* Chopstick,隨後在吃飯上花掉一段隨機化的時間,之後重複此過程。
*
* @create @author Henry @date 2016-12-26
*
*/
public class Philosopher implements Runnable {
private Chopstick left;
private Chopstick right;
private final int id;
private final int ponderFactor;
private Random rand = new Random(47);
public Philosopher(Chopstick left, Chopstick right, int id, int ponderFactor) {
this.left = left;
this.right = right;
this.id = id;
this.ponderFactor = ponderFactor;
}
private void pause() throws InterruptedException {
if (ponderFactor == 0)
return;
TimeUnit.MILLISECONDS.sleep(rand.nextInt(ponderFactor * 250));
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
System.out.println(this + " " + "thinking");
pause();
// Philosopher becomes hungry
System.out.println(this + " " + "grabbing right");
right.take();
System.out.println(this + " " + "grabbing left");
left.take();
System.out.println(this + " " + "eating");
pause();
right.drop();
left.drop();
}
} catch (InterruptedException e) {
System.out.println(this + " " + "exiting via interrupt");
}
}
@Override
public String toString() {
return "Philosopher " + id;
}
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 現在我們可以建立這個程式的將會產生死鎖的版本了:
*
* 你會發現,如果Philosopher花在思考上的時間非常少,那麼當他們想要進餐時,全部會在Chopstick上產生競爭,而死鎖也就更快地發生。
*
* @create @author Henry @date 2016-12-26
*
*/
public class DeadLockingDiningPhilosophers {
public static void main(String[] args) throws Exception {
int ponder=0;
int size=5;
ExecutorService exec=Executors.newCachedThreadPool();
Chopstick[] sticks=new Chopstick[size];
for(int i=0;i<size;i++)
sticks[i]=new Chopstick();
for(int i=0;i<size;i++)
exec.execute(new Philosopher(sticks[i], sticks[(i+1)%size], i, ponder));
TimeUnit.SECONDS.sleep(5);
//exec.shutdownNow();
}
}
第一個可以調整ponder因子,從而影響每個Philosopher花費在思考上的時間長度。如果有許多Philosopher,或者他們花費很多時間去思考,那麼儘管存在死鎖的可能,但你可能永遠也看不到死鎖。值為0的ponder傾向於使死鎖儘快發生。
注意,Chopstick物件不需要內部識別符號,它們是由在陣列sticks中的位置來標識的。每個Philosopher構造器都會得到一個對左邊和右邊Chopstick物件的引用。除了最後一個Philosopher,其他所有的Philosopher都是通過將這個Philosopher定位於下一對Chopstick物件之間而被初始化的,而最後一個Philosopher右邊的Chopstick是第0個Chopstick,這樣這個迴圈表也就結束了。因為最後一個Philosopher坐在第一個Philosopher的右邊,所以他們會共享第0個Chopstick。現在,所有的Philosopher都有可能希望進餐,從而等待其臨近的Philosopher放下他們的Chopstick。這將使程式死鎖。
如果Philosopher花費更多的時間去思考而不是進餐(使用非0的ponder值,或者大量的Philosopher),那麼他們請求共享資源(chopstick)可能性就會小很多,這樣你就會確信該程式不會死鎖,儘管他們並非如此。這個示例相當有趣,因為它演示了看起來可以正確執行,但實際上會死鎖的程式。
要修正死鎖問題,你必須明白,當以下四個條件同時滿足時,就會發生死鎖:
1.互斥條件。任務使用的資源中至少有一個是不能共享的。這裡,一根Chopstick一次就只能被一個Philosopher使用。
2.至少有一個任務它必須持有一個資源且正在等待獲取一個當前被別的任務持有的資源。也就是說,要發生死鎖,Philosopher必須拿著一根Chopstick並且等待另一根。
3.資源不能被任務搶佔,任務必須把資源釋放當作普通事件。Philosopher很有禮貌,他們不會從其他Philosopher那裡搶Chopstick。
4.必須有迴圈等待,這時,一個任務等待其他任務所持有的資源,後者又在等待另一個任務所持有的資源,這樣一直下去,直到有一個任務在等待第一個任務所持有的資源,使得大家都被鎖住。在DeadlockingDiningPhilosophers.java中,因為每個Philosopher都試圖先得到右邊的Chopstick,然後得到左邊的Chopstick,所以發生了迴圈等待。
因為要發生死鎖的話,所以這些條件必須全部滿足;所以要防止死鎖的話,只需破壞其中一個即可。在程式中,防止死鎖最容易的方法就是破壞第四個條件。有這個條件的原因是每個Philosopher都試圖用特定的順序拿Chopstick:先右後左。正因為如此,就可能會發生“每個人都拿著右邊的Chopstick,並且等待左邊的Chopstick”的情況,這就是迴圈等待條件。然而如果最後一個Philosopher被初始化成先拿左邊的Chopstick,後拿右邊的Chopstick,那麼這個Philosopher將永遠不會阻止其右邊的Philosopher拿起他們的Chopstick。在本例中,這就可以防止迴圈的等待。這只是問題的解決方法之一,也可以通過破壞其他條件來防止死鎖(具體細節請參考更高階的討論執行緒的書籍):
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 通過確保最後一個Philosopher先拿起和放下左邊的Chopstick,我們可以移除死鎖,從而使這個程式平滑地執行。
*
*
* @create @author Henry @date 2016-12-26
*
*/
public class FixedDiningPhilosophers {
public static void main(String[] args) throws Exception {
int ponder = 0;
int size = 5;
ExecutorService exec = Executors.newCachedThreadPool();
Chopstick[] sticks = new Chopstick[size];
for (int i = 0; i < size; i++)
sticks[i] = new Chopstick();
for (int i = 0; i < size; i++)
if (i < (size - 1))
exec.execute(new Philosopher(sticks[i], sticks[i + 1], i, ponder));
else
exec.execute(new Philosopher(sticks[0], sticks[i], i, ponder));
TimeUnit.SECONDS.sleep(5);
exec.shutdownNow();
}
}
Java對死鎖並沒有提供語音層面上的支援;能否通過仔細地設計程式來避免死鎖,這取決於你自己。對於真正檢視除錯一個有死鎖程式的程式設計師來說,這不是什麼安慰人的話。