Q:5 個沉默寡言的哲學家圍坐在圓桌前,每人面前一盤意面。叉子放在哲學家之間的桌面上。(5 個哲學家,5 根叉子)
所有的哲學家都只會在思考和進餐兩種行為間交替。哲學家只有同時拿到左邊和右邊的叉子才能吃到面,而同一根叉子在同一時間只能被一個哲學家使用。每個哲學家吃完面后都需要把叉子放回桌面以供其他哲學家吃面。只要條件允許,哲學家可以拿起左邊或者右邊的叉子,但在沒有同時拿到左右叉子時不能進食。
假設面的數量沒有限制,哲學家也能隨便吃,不需要考慮吃不吃得下。
設計一個進餐規則(並行算法)使得每個哲學家都不會挨餓;也就是說,在沒有人知道別人什么時候想吃東西或思考的情況下,每個哲學家都可以在吃飯和思考之間一直交替下去。
哲學家從 0 到 4 按 順時針 編號。請實現函數 void wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork):
philosopher 哲學家的編號。
pickLeftFork 和 pickRightFork 表示拿起左邊或右邊的叉子。
eat 表示吃面。
putLeftFork 和 putRightFork 表示放下左邊或右邊的叉子。
由於哲學家不是在吃面就是在想着啥時候吃面,所以思考這個方法沒有對應的回調。
給你 5 個線程,每個都代表一個哲學家,請你使用類的同一個對象來模擬這個過程。在最后一次調用結束之前,可能會為同一個哲學家多次調用該函數。
示例:
輸入:n = 1
輸出:[[4,2,1],[4,1,1],[0,1,1],[2,2,1],[2,1,1],[2,0,3],[2,1,2],[2,2,2],[4,0,3],[4,1,2],[0,2,1],[4,2,2],[3,2,1],[3,1,1],[0,0,3],[0,1,2],[0,2,2],[1,2,1],[1,1,1],[3,0,3],[3,1,2],[3,2,2],[1,0,3],[1,1,2],[1,2,2]]
解釋:
n 表示每個哲學家需要進餐的次數。
輸出數組描述了叉子的控制和進餐的調用,它的格式如下:
output[i] = [a, b, c] (3個整數)
- a 哲學家編號。
- b 指定叉子:{1 : 左邊, 2 : 右邊}.
- c 指定行為:{1 : 拿起, 2 : 放下, 3 : 吃面}。
如 [4,2,1] 表示 4 號哲學家拿起了右邊的叉子。
A:
引用:@̶.̶G̶F̶u̶'̶ 、̶ ̶|
這個題目是防止死鎖,每個哲學家都拿起左手或右手,導致死鎖
1.第一種方法是設置一個信號量,當前哲學家會同時拿起左手和右手的叉子直至吃完。即有3 個人中,2 個人各自持有 2 個叉子,1 個人持有 1 個叉子,共計 5 個叉子。
用Semaphore去實現上述的限制:Semaphore eatLimit = new Semaphore(4);
一共有5個叉子,視為5個ReentrantLock,並將它們全放入1個數組中。
設置編碼:
代碼:
class DiningPhilosophers {
//1個Fork視為1個ReentrantLock,5個叉子即5個ReentrantLock,將其都放入數組中
private ReentrantLock[] locks = {new ReentrantLock(), new ReentrantLock(), new ReentrantLock(), new ReentrantLock(), new ReentrantLock()};
//限制 最多只有4個哲學家去持有叉子
private Semaphore eatLimit = new Semaphore(4);
public DiningPhilosophers() {
}
// call the run() method of any runnable to execute its code
public void wantsToEat(int philosopher,
Runnable pickLeftFork,
Runnable pickRightFork,
Runnable eat,
Runnable putLeftFork,
Runnable putRightFork) throws InterruptedException {
int leftFork = (philosopher + 1) % 5;//左邊的叉子 的編號
int rightFork = philosopher;//右邊的叉子 的編號
eatLimit.acquire();//限制人數減一
locks[leftFork].lock();
locks[rightFork].lock();
pickLeftFork.run();
pickRightFork.run();
eat.run();
putLeftFork.run();
putRightFork.run();
locks[leftFork].unlock();
locks[rightFork].unlock();
eatLimit.release();
}
}
2.設置 1 個臨界區以實現 1 個哲學家 “同時”拿起左右 2 把叉子的效果。即進入臨界區之后,保證成功獲取到左右 2 把叉子 並 執行相關代碼后,才退出臨界區。
與上一種的差別是“允許1個哲學家用餐”。方法2是在成功拿起左右叉子之后就退出臨界區,而“只讓1個哲學家就餐”是在拿起左右叉子 + 吃意面 + 放下左右叉子 一套流程走完之后才退出臨界區。
前者的情況可大概分為2種,舉具體例子說明(可參照上面給出的圖片):
- 1號哲學家拿起左右叉子(1號叉子 + 2號叉子)后就退出臨界區,此時4號哲學家成功擠進臨界區,他也成功拿起了左右叉子(0號叉子和4號叉子),然后就退出臨界區。
- 1號哲學家拿起左右叉子(1號叉子 + 2號叉子)后就退出臨界區,此時2號哲學家成功擠進臨界區,他需要拿起2號叉子和3號叉子,但2號叉子有一定的概率還被1號哲學家持有(1號哲學家意面還沒吃完),因此2號哲學家進入臨界區后還需要等待2號叉子。至於3號叉子,根本沒其他人跟2號哲學家爭奪,因此可以將該種情況視為“2號哲學家只拿起了1只叉子,在等待另1只叉子”的情況。
總之,第1種情況即先后進入臨界區的2位哲學家的左右叉子不存在競爭情況,因此先后進入臨界區的2位哲學家進入臨界區后都不用等待叉子,直接就餐。此時可視為2個哲學家在同時就餐(當然前1個哲學家有可能已經吃完了,但姑且當作是2個人同時就餐)。
第2種情況即先后進入臨界區的2位哲學家的左右叉子存在競爭情況(說明這2位哲學家的編號相鄰),因此后進入臨界區的哲學家還需要等待1只叉子,才能就餐。此時可視為只有1個哲學家在就餐。
至於“只允許1個哲學家就餐”的代碼,很好理解,每次嚴格地只讓1個哲學家就餐,由於過於嚴格,以至於都不需要將叉子視為ReentrantLock。
方法2有一定的概率是“並行”,“只允許1個哲學家就餐”是嚴格的“串行”。
代碼:
class DiningPhilosophers {
//1個Fork視為1個ReentrantLock,5個叉子即5個ReentrantLock,將其都放入數組中
private ReentrantLock[] lockList = {new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock()};
//讓 1個哲學家可以 “同時”拿起2個叉子(搞個臨界區)
private ReentrantLock pickBothForks = new ReentrantLock();
public DiningPhilosophers() {
}
// call the run() method of any runnable to execute its code
public void wantsToEat(int philosopher,
Runnable pickLeftFork,
Runnable pickRightFork,
Runnable eat,
Runnable putLeftFork,
Runnable putRightFork) throws InterruptedException {
int leftFork = (philosopher + 1) % 5; //左邊的叉子 的編號
int rightFork = philosopher; //右邊的叉子 的編號
pickBothForks.lock(); //進入臨界區
lockList[leftFork].lock(); //拿起左邊的叉子
lockList[rightFork].lock(); //拿起右邊的叉子
pickLeftFork.run(); //拿起左邊的叉子 的具體執行
pickRightFork.run(); //拿起右邊的叉子 的具體執行
pickBothForks.unlock(); //退出臨界區
eat.run(); //吃意大利面 的具體執行
putLeftFork.run(); //放下左邊的叉子 的具體執行
putRightFork.run(); //放下右邊的叉子 的具體執行
lockList[leftFork].unlock(); //放下左邊的叉子
lockList[rightFork].unlock(); //放下右邊的叉子
}
}
3.前面說過,該題的本質是考察 如何避免死鎖。
而當5個哲學家都左手持有其左邊的叉子 或 當5個哲學家都右手持有其右邊的叉子時,會發生死鎖。
故只需設計1個避免發生上述情況發生的策略即可。
即可以讓一部分哲學家優先去獲取其左邊的叉子,再去獲取其右邊的叉子;再讓剩余哲學家優先去獲取其右邊的叉子,再去獲取其左邊的叉子。
代碼:
class DiningPhilosophers {
//1個Fork視為1個ReentrantLock,5個叉子即5個ReentrantLock,將其都放入數組中
private ReentrantLock[] lockList = {new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock()};
public DiningPhilosophers() {
}
// call the run() method of any runnable to execute its code
public void wantsToEat(int philosopher,
Runnable pickLeftFork,
Runnable pickRightFork,
Runnable eat,
Runnable putLeftFork,
Runnable putRightFork) throws InterruptedException {
int leftFork = (philosopher + 1) % 5; //左邊的叉子 的編號
int rightFork = philosopher; //右邊的叉子 的編號
//編號為偶數的哲學家,優先拿起左邊的叉子,再拿起右邊的叉子
if (philosopher % 2 == 0) {
lockList[leftFork].lock(); //拿起左邊的叉子
lockList[rightFork].lock(); //拿起右邊的叉子
}
//編號為奇數的哲學家,優先拿起右邊的叉子,再拿起左邊的叉子
else {
lockList[rightFork].lock(); //拿起右邊的叉子
lockList[leftFork].lock(); //拿起左邊的叉子
}
pickLeftFork.run(); //拿起左邊的叉子 的具體執行
pickRightFork.run(); //拿起右邊的叉子 的具體執行
eat.run(); //吃意大利面 的具體執行
putLeftFork.run(); //放下左邊的叉子 的具體執行
putRightFork.run(); //放下右邊的叉子 的具體執行
lockList[leftFork].unlock(); //放下左邊的叉子
lockList[rightFork].unlock(); //放下右邊的叉子
}
}
改進:改進代碼看3種解法(互斥鎖或volatile)
1.ReentrantLock和synchronize關鍵字都是使用互斥量的重量級鎖,而volatile關鍵字相較於它們就比較“輕量”。
因此把ReentrantLock數組改為使用volatile修飾的boolean數組。
PS: volatile要和原子操作搭配使用才能保證同步。
而對volatile變量賦 常量值 可看為是原子操作。
看着后面這種解法更清晰:
每個人都可以嘗試去吃東西,吃東西前嘗試去拿左邊的叉子和右邊的叉子,這樣就可以想到使用信號量Semaphore的tryAcquire方法。
這里競爭的資源是叉子,所以定義代表5個叉子的信號量即可。
代碼:
class DiningPhilosophers {
int num = 5;
//五個叉子的信號量
private Semaphore[] semaphores = new Semaphore[5];
public DiningPhilosophers() {
for (int i = 0; i < num; i++) {
//每只叉子只有1個
semaphores[i] = new Semaphore(1);
}
}
// call the run() method of any runnable to execute its code
public void wantsToEat(int philosopher,
Runnable pickLeftFork,
Runnable pickRightFork,
Runnable eat,
Runnable putLeftFork,
Runnable putRightFork) throws InterruptedException {
//左邊叉子的位置
int left = philosopher;
//右邊叉子的位置
int right = (philosopher + 1) % num;
while (true) {
if (semaphores[left].tryAcquire()) {
//先嘗試獲取左邊叉子,如果成功再嘗試獲取右邊叉子
if (semaphores[right].tryAcquire()) {
//兩個叉子都得到了,進餐
pickLeftFork.run();
pickRightFork.run();
eat.run();
putLeftFork.run();
//釋放左邊叉子
semaphores[left].release();
putRightFork.run();
//釋放右邊邊叉子
semaphores[right].release();
//吃完了,就跳出循環
break;
} else {
//如果拿到了左邊的叉子,但沒拿到右邊的叉子: 就釋放左邊叉子
semaphores[left].release();
//讓出cpu等一會
Thread.yield();
}
} else {
//連左邊叉子都沒拿到,就讓出cpu等會吧
Thread.yield();
}
}
}
}