文章首發於公衆號,歡迎訂閱
問題描述
哲學家就餐問題(Dining philosophers problem)是在計算機科學中的一個經典問題,用來演示在併發計算中多線程同步時產生的問題。
在1971年,著名的計算機科學家艾茲格·迪科斯徹提出了一個同步問題,即假設有五臺計算機都試圖訪問五份共享的磁帶驅動器。稍後,這個問題被託尼·霍爾重新表述爲哲學家就餐問題。這個問題可以用來解釋死鎖和資源耗盡。
哲學家就餐問題可以這樣表述,假設有五位哲學家圍坐在一張圓形餐桌旁,做以下兩件事情之一:吃飯,或者思考。吃東西的時候,他們就停止思考,思考的時候也停止吃東西。餐桌中間有一大碗意大利麪,每兩個哲學家之間有一隻餐叉。因爲用一隻餐叉很難吃到意大利麪,所以假設哲學家必須用兩隻餐叉吃東西。他們只能使用自己左右手邊的那兩隻餐叉。哲學家就餐問題有時也用米飯和筷子而不是意大利麪和餐叉來描述,因爲很明顯,吃米飯必須用兩根筷子。
哲學家從來不交談,這就很危險,可能產生死鎖,每個哲學家都拿着左手的餐叉,永遠都在等右邊的餐叉(或者相反)。
即使沒有死鎖,也有可能發生資源耗盡。例如,假設規定當哲學家等待另一隻餐叉超過五分鐘後就放下自己手裏的那一隻餐叉,並且再等五分鐘後進行下一次嘗試。這個策略消除了死鎖(系統總會進入到下一個狀態),但仍然有可能發生活鎖。如果五位哲學家在完全相同的時刻進入餐廳,並同時拿起左邊的餐叉,那麼這些哲學家就會等待五分鐘,同時放下手中的餐叉,再等五分鐘,又同時拿起這些餐叉。
在實際的計算機問題中,缺乏餐叉可以類比爲缺乏共享資源。一種常用的計算機技術是資源加鎖,用來保證在某個時刻,資源只能被一個程序或一段代碼訪問。當一個程序想要使用的資源已經被另一個程序鎖定,它就等待資源解鎖。當多個程序涉及到加鎖的資源時,在某些情況下就有可能發生死鎖。例如,某個程序需要訪問兩個文件,當兩個這樣的程序各鎖了一個文件,那它們都在等待對方解鎖另一個文件,而這永遠不會發生。
死鎖的必要條件
死鎖的產生具備以下四個條件:
- 互斥條件:指線程對己經獲取到的資源進行排它性使用, 即該資源同時只由一個線程佔用。如果此時還有其他線程請求獲取該資源,則請求者只能等待,直至佔有資源的線程釋放該資源。
- 請求並持有條件: 指一個線程己經持有了至少一個資源 , 但又提出了新的資源請求 ,而新資源己被其他線程佔有,所以當前線程會被阻塞,但阻塞的同時並不釋放自己己經獲取的資源。
- 不可剝奪條件: 指線程獲取到的資源在自己使用完之前不能被其他線程搶佔,只有在自己使用完畢後才由自己釋放該資源。
- 環路等待條件:指在發生死鎖時,必然存在一個線程→資源的環形鏈,即線程集合{ T0,T1,T2 ,…,Tn }中的 T0 正在等待一個 T1 佔用的資源,T1 正在等待 T2 佔用的資源,……Tn 正在等待己被 T0 佔用的資源。
復現死鎖
當所有哲學家同時決定進餐,拿起左邊筷子時候,就發生了死鎖。
public class Problem {
public static void main(String[] args) {
int sum = 5;
Philosopher[] philosophers = new Philosopher[sum];
Chopstick[] chopsticks = new Chopstick[sum];
for (int i = 0; i < sum; i++) {
chopsticks[i] = new Chopstick();
}
for (int i = 0; i < sum; i++) {
Chopstick left = chopsticks[i];
Chopstick right = chopsticks[(i + 1) % sum];
philosophers[i] = new Philosopher(left, right);
new Thread(philosophers[i], "哲學家" + (i + 1) + "號").start();
}
}
}
class Chopstick {
}
class Philosopher implements Runnable {
private final Chopstick left;
private final Chopstick right;
public Philosopher(Chopstick left, Chopstick right) {
this.left = left;
this.right = right;
}
@Override
public void run() {
try {
while (true) {
doAction("思考");
synchronized (left) {
doAction("拿起左邊筷子");
synchronized (right) {
doAction("拿起右邊筷子--------開吃了");
doAction("吃完了,放下筷子");
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 10));
}
}
解決方法
資源分級算法(破壞死鎖的環路等待條件)
資源分級算法是指爲資源分配一個偏序或者分級的關係,並約定所有資源都按照這種順序獲取,按相反順序釋放。對應在哲學家就餐問題中就是爲各個餐叉設置 1 - 2 - 3 - 4 - 5 的序號,每一個哲學家總是先拿起左右兩邊編號較低的餐叉,再拿編號較高的。用完餐叉後,他總是先放下編號較高的餐叉,再放下編號較低的。在這種情況下,1 ~ 4 號哲學家都是左邊的餐叉序號小,而 5 號哲學家是右邊的餐叉序號小,當 1 ~ 4 號哲學家同時拿起他們手邊編號較低的餐叉即 1~4 號餐叉時,只有編號最高的 5 號餐叉留在桌上,5 號哲學家先申請序號較小的 1 號,發現已經被拿走,所以他就只能等待。而剩下的那支 5 號餐叉被 4 號哲學家成功獲得。當 4 號哲學家吃完後,他會先放下編號最高的餐叉,再放下編號較低的餐叉,從而使得 3 號哲學家成功獲得他所需的第二支餐叉,以此類推,整個系統不會發生死鎖。實際執行順序還是要看 CPU 的分配,不過這樣已經不會構成循環了。
此處給筷子添加 id,根據 id 從小到大獲取(不用關心編號的具體規則,只要保證編號是全局唯一併且有序的)。
代碼如下:
public class Solution1 {
public static void main(String[] args) {
int sum = 5;
Philosopher[] philosophers = new Philosopher[sum];
Chopstick[] chopsticks = new Chopstick[sum];
for (int i = 0; i < sum; i++) {
chopsticks[i] = new Chopstick(i);
}
for (int i = 0; i < sum; i++) {
Chopstick left = chopsticks[i];
Chopstick right = chopsticks[(i + 1) % sum];
philosophers[i] = new Philosopher(left, right);
new Thread(philosophers[i], "哲學家" + (i + 1) + "號").start();
}
}
}
class Chopstick {
private int id;
public Chopstick(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
class Philosopher implements Runnable {
private final Chopstick left;
private final Chopstick right;
public Philosopher(Chopstick left, Chopstick right) {
if (left.getId() < right.getId()) {
this.left = left;
this.right = right;
} else {
this.left = right;
this.right = left;
}
}
@Override
public void run() {
try {
while (true) {
doAction("思考");
synchronized (left) {
doAction("拿起左邊筷子");
synchronized (right) {
doAction("拿起右邊筷子--------開吃了");
doAction("吃完了,放下筷子");
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 10));
}
}
破壞死鎖的請求並持有條件
1、使用多把鎖,每把鎖使用 tryLock
爲獲取鎖操作設置超時時間。
代碼如下:
public class Solution2 {
public static void main(String[] args) {
int sum = 5;
Philosopher[] philosophers = new Philosopher[sum];
ReentrantLock[] chopsticks = new ReentrantLock[sum];
for (int i = 0; i < sum; i++) {
chopsticks[i] = new ReentrantLock();
}
for (int i = 0; i < sum; i++) {
ReentrantLock left = chopsticks[i];
ReentrantLock right = chopsticks[(i + 1) % sum];
philosophers[i] = new Philosopher(left, right);
new Thread(philosophers[i], "哲學家" + (i + 1) + "號").start();
}
}
}
class Philosopher implements Runnable {
private final ReentrantLock left;
private final ReentrantLock right;
public Philosopher(ReentrantLock left, ReentrantLock right) {
this.left = left;
this.right = right;
}
@Override
public void run() {
try {
while (true) {
doAction("思考");
left.lock();
try {
doAction("拿起左邊筷子");
if (right.tryLock(10, TimeUnit.MILLISECONDS)) {
try {
doAction("拿起右邊筷子--------開吃了");
} finally {
right.unlock();
doAction("吃完了,放下筷子");
}
} else {
// 沒有獲取到右手的筷子,放棄並繼續思考
}
} finally {
left.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 10));
}
}
2、使用一把鎖,設置條件隊列 Condition
。
該方法只用一把鎖,沒有 Chopstick
類,將競爭從對筷子的爭奪轉換成了對狀態的判斷。僅當左右鄰座都沒有進餐時纔可以進餐。
public class Solution3 {
public static void main(String[] args) {
int sum = 5;
Philosopher[] philosophers = new Philosopher[sum];
ReentrantLock lock = new ReentrantLock();
// 安排哲學家就坐
for (int i = 0; i < sum; i++) {
philosophers[i] = new Philosopher(lock);
}
// 設置哲學家的左右鄰居
for (int i = 0; i < sum; i++) {
Philosopher left = philosophers[(i + sum) % sum];
Philosopher right = philosophers[(i + 1) % sum];
philosophers[i].setLeft(left);
philosophers[i].setRight(right);
new Thread(philosophers[i], "哲學家" + (i + 1) + "號").start();
}
}
}
class Philosopher implements Runnable {
private boolean eating;
private Philosopher left;
private Philosopher right;
private final ReentrantLock lock;
private final Condition condition;
public Philosopher(ReentrantLock lock) {
eating = false;
this.lock = lock;
this.condition = lock.newCondition();
}
public void setLeft(Philosopher left) {
this.left = left;
}
public void setRight(Philosopher right) {
this.right = right;
}
public void think() throws InterruptedException {
lock.lock();
try {
eating = false;
System.out.println(Thread.currentThread().getName() + "開始思考");
left.condition.signal();
right.condition.signal();
} finally {
lock.unlock();
}
Thread.sleep(10);
}
public void eat() throws InterruptedException {
lock.lock();
try {
// 左右兩邊只要有任意哲學家在吃飯,就等待
while (left.eating || right.eating) {
condition.await();
}
System.out.println(Thread.currentThread().getName() + "開始吃飯");
eating = true;
} finally {
lock.unlock();
}
Thread.sleep(10);
}
@Override
public void run() {
try {
while (true) {
think();
eat();
}
} catch (InterruptedException e) {
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 10));
}
}
參考文章https://www.jianshu.com/p/99f10708b1e1
更多解法可以參考leetcode
我創建了一個免費的知識星球,用於分享知識日記,歡迎加入!