一個物件可以有 synchronized 方法或其他形式的加鎖機制來防止別的任務在互斥還沒有釋放的時候就訪問這個物件。
死鎖
任務有可能變成阻塞狀態,所以就可能發生這樣的情況:某個任務在等待另一個任務,而後者又在等待別的任務,這樣一直下去,直到這個鏈條上的任務又在等待第一個任務釋放鎖。這就形成了一個相互等待的迴圈,沒有那個執行緒能夠繼續。這被稱之為死鎖。
我們真正需要解決的問題是程式看起來可能工作良好,但是具有潛在的死鎖風險。這時,死鎖可能發生,而事先卻沒有任何徵兆,所以缺陷會潛伏在你的程式裡,直到被人以外的發現了。因此,在編寫併發程式設計的時候,進行仔細的程式設計以防止死鎖是非常關鍵的。
下面引入一個問題,一共有 5 個哲學家。這些哲學家將花部分時間思考,花部分時間就餐。作為哲學家他們很窮,所以他們只能買 5 根筷子。他們圍坐在桌子的周圍,每人之間放一根筷子。當一個哲學家要就餐的時候,這個哲學家必須同時得到左邊和右邊的筷子。如果一個哲學家左邊或者右邊已經得到筷子,那麼這個哲學家就必須等待,直至可得到必須的筷子。
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();
}
}
複製程式碼
任何兩個哲學家都不能使用同一根筷子。也就是不能同時 taken() 同一個筷子。另外如果一個 Chopstick 被一個哲學家獲得,那麼另一個哲學家可以 wait(),直到當前的這根筷子的持有者呼叫 drop() 結束使用。
public class Philosopher implements Runnable{
private Chopstick left;
private Chopstick right;
private final int id;
private final int ponderFactor;
private Random random = new Random(47);
public void pause() throws InterruptedException{
if (ponderFactor ==0) {
return;
}
TimeUnit.MICROSECONDS.sleep(random.nextInt(ponderFactor * 250));
}
protected Philosopher(Chopstick left, Chopstick right, int id, int ponderFactor) {
super();
this.left = left;
this.right = right;
this.id = id;
this.ponderFactor = ponderFactor;
}
@Override
public void run() {
// TODO Auto-generated method stub
try {
while (!Thread.interrupted()) {
System.out.println(this+"開始思考");
pause();
System.out.println(this+"開始拿左邊的筷子");
left.take();
System.out.println(this+"開始拿右邊的筷子");
right.take();
System.out.println(this+"開始就餐");
pause();
left.drop();
right.drop();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println("當前執行緒被中斷了");
}
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "哲學家的編號:"+id;
}
}
複製程式碼
在哲學家的任務中,每個哲學家都是不斷的思考和吃飯。如果 ponderFactor 不為 0,則 pause() 就會休眠一會。通過這樣的方法你會看到哲學家會思考一段時間。然後嘗試著去獲取左邊和右邊的筷子,隨後再在吃飯上花掉一段隨機的時間,之後重複此過程。
現在我們來建立這個程式的死鎖版本:
public class DeadLockingDiningPhilosophers {
public static void main(String[] args) {
// TODO Auto-generated method stub
int ponder = 5;
int size = 5;
ExecutorService service = Executors.newCachedThreadPool();
Chopstick[] sChopsticks = new Chopstick[size];
for (int i = 0; i < size; i++) {
sChopsticks[i] = new Chopstick();
}
for (int i = 0; i < size; i++) {
//每一個哲學家都會持有他左邊和右邊的筷子物件
service.execute(new Philosopher(sChopsticks[i],sChopsticks[(i+1)%size],i,ponder));
}
System.out.println("執行結束");
service.shutdownNow();
}
}
複製程式碼
執行的結果:
執行結束
哲學家的編號:2開始思考
哲學家的編號:4開始思考
哲學家的編號:1開始思考
哲學家的編號:0開始思考
哲學家的編號:3開始思考
當前執行緒被中斷了
當前執行緒被中斷了
當前執行緒被中斷了
當前執行緒被中斷了
當前執行緒被中斷了
複製程式碼
這個程式表示每一個哲學家都有可能要表示進餐,從而等待其臨近的 Philosopher 放下他們的 Chopstick。這將會使得程式死鎖。
要修正死鎖必須明白,當以下四個條件同時滿足時,就會發生死鎖:
- 互斥條件。任務使用的資源中至少有一個是不能共享的。
- 至少有一個任務必須持有一個資源,並且正在等待獲取一個當前被別的任務持有的資源。也就是說必須是拿著一根筷子等待另一個筷子。
- 資源不能被任務搶佔,任務必須把資源釋放當做普通事件。你不能搶別人手裡的筷子。
- 必須有迴圈等待,這時一個任務等待其他任務所持有的資源,後者又在等待另一個任務所持有的資源,這樣迴圈下去直到有一個任務等待第一個任務所持有的資源,使得大家都被鎖住。
因為要發生死鎖所有這些條件必須滿足;所以要防止死鎖的話只需要破壞其中一個就可以。在程式中防止死鎖的最容易的辦法就是破壞第四個迴圈條件。
public class FixedDiningPhilosophers {
public static void main(String[] args) throws Exception {
int ponder = 5;
if(args.length > 0)
ponder = Integer.parseInt(args[0]);
int size = 5;
if(args.length > 1)
size = Integer.parseInt(args[1]);
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));
if(args.length == 3 && args[2].equals("timeout"))
TimeUnit.SECONDS.sleep(5);
else {
System.out.println("Press 'Enter' to quit");
System.in.read();
}
exec.shutdownNow();
}
}
複製程式碼
執行結果:
哲學家的編號:2開始拿左邊的筷子
哲學家的編號:4開始思考
哲學家的編號:1開始思考
哲學家的編號:0開始拿左邊的筷子
哲學家的編號:0開始拿右邊的筷子
哲學家的編號:0開始就餐
哲學家的編號:3開始就餐
哲學家的編號:2開始拿右邊的筷子
/.....
複製程式碼
通過確保最後一個哲學家先拿起和放下左邊的筷子,我們可以移除死鎖,從而使得程式執行。Java 並沒有對死鎖提供類庫上的支援;能否通過仔細的程式設計避免死鎖需要我們自己努力。
掃碼關注公眾號即可立即獲取全套的 Java 程式設計思想讀書筆記。