一张圆桌上坐着 5 名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。
哲学家们倾注毕生的精力用于思考和进餐,哲学家在思考时,并不影响他人。
只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。
饥饿的哲学家只有同时拿起两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。
1、问题分析
-
关系分析。系统中有5个哲学家进程,5位哲学家与左右邻居对其中间筷子的访问是互斥关系。
-
整理思路。这个问题中只有互斥关系,但与之前遇到的问题不同的事,每个哲学家进程需要同时持有两个临界资源才能开始吃饭。如何避免临界资源分配不当造成的死锁现象,是哲学家问题的精髓。
-
信号量设置。定义互斥信号量数组
chopstick[5]={1,1,1,1,1}
用于实现对 5 个筷子的互斥访问。并对哲学家按 0~4 编号,哲学家i左边的筷子编号为
i
,右边的筷子编号为(i+1)%5
。
错误
循环的等待
2、如何避免死锁的发生呢?(附 Java 实现)
① 可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐。(破坏循环等待条件,本来 5 个哲学家对应 5 个筷子,现在 4 个哲学家对应 5 个筷子)
这样可以保证至少有一个哲学家是可以拿到左右两只筷子的
semaphore chopstick [5] = {1,1,1,1,1};
semaphore mutex = 4; //只允许 4 个哲学家进行争抢
pi () {
while (1) {
P(mutex);
P(chopstick[i]); //拿左
P(chopstick[(i + 1) % 5]); //拿右
V(mutex); //V 顺序无所谓
吃饭...
V(chopstick[i]); //放左
V(chopstick[(i + 1) % 5]); //放右
思考...
}
}
Java 实现
/**
* 最多允许四个哲学家同时进餐
*/
class DiningPhilosophers {
public DiningPhilosophers() {
}
Lock[] chopstick = new ReentrantLock[]{
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock()
};
Semaphore mutex = new Semaphore(4);
// 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 {
//信号量 -1
mutex.acquire();
//拿起左右筷子
chopstick[philosopher].lock();
chopstick[(philosopher + 1) % 5].lock();
pickLeftFork.run();
pickRightFork.run();
//eat
eat.run();
//放下左右筷子
putLeftFork.run();
putRightFork.run();
chopstick[philosopher].unlock();
chopstick[(philosopher + 1) % 5].unlock();
//信号量 + 1
mutex.release();
}
}
② 要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。(破坏循环等待,分为奇偶数,给上编号)
用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞。这就避免了占有一支后再等待另一只的情况。
semaphore chopstick [5] = {1,1,1,1,1};
semaphore mutex = 1; //互斥地取筷子
pi () {
while (1) {
P(mutex);
//偶数
if (i & 1 == 0) {
P(chopstick[(i + 1) % 5]); //拿右
P(chopstick[i]); //拿左
}else {
P(chopstick[i]); //拿左
P(chopstick[(i + 1) % 5]); //拿右
}
V(mutex); //V 顺序无所谓
吃饭...
V(chopstick[i]); //放左
V(chopstick[(i + 1) % 5]); //放右
思考...
}
}
Java 实现
/**
* 奇数号哲学家先左后右, 偶数号哲学家先右后左
*/
class DiningPhilosophers {
public DiningPhilosophers() {
}
Lock[] chopstick = new ReentrantLock[]{
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock()
};
// 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 {
//尝试拿起左右筷子
if ((philosopher & 1) != 0) {
chopstick[philosopher].lock();
chopstick[(philosopher + 1) % 5].lock();
}else {
chopstick[(philosopher + 1) % 5].lock();
chopstick[philosopher].lock();
}
pickLeftFork.run();
pickRightFork.run();
//eat
eat.run();
putLeftFork.run();
putRightFork.run();
//放下左右筷子
chopstick[philosopher].unlock();
chopstick[(philosopher + 1) % 5].unlock();
}
}
③ 仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子。(破坏请求和保持条件,预先分配 2 个筷子)
如下所示:
-
若 0 号哲学家正在
P(chopstick[i])
中,此时 2 号哲学家也想拿筷子,将会阻塞在P(mutex)
,直到等待 0 号哲学家V(mutex)
-
若 0 号哲学家正在吃饭,0, 1 号筷子被使用;此时 1 号哲学家将会阻塞
P(chopstick[i])
;(0 号筷子),mutex = 0
; 若 2 号哲学家也想拿筷子,则会被阻塞到P(mutex)
- 即使 2 号左右两边的筷子都在,也暂时无法取得
-
若 0 号哲学家正在吃饭,0, 1 号筷子被使用;此时 4 号哲学家将拿起左边筷子,但是阻塞在
P(chopstick[(i + 1) % 5])
,直到等待 0 号哲学家吃完饭V(chopstick[i]);
- 此时 4 号右边的筷子不可用,但 4 号仍然会拿起左边的筷子
semaphore chopstick [5] = {1,1,1,1,1};
semaphore mutex =1; //互斥地取筷子
Pi () { //i 号哲学家的进程
while (1) {
P(mutex);
P(chopstick[i]); //拿左
P(chopstick[(i + 1) % 5]); //拿右
V(mutex);
吃饭...
V(chopstick[i]); //放左
V(chopstick[(i + 1) % 5]); //放右
思考...
}
}
因此上述方法并不能保证只有两边的筷子都可用时,才允许哲学家拿起筷子
- 例如上述的情况 1 和 2 都会导致 2 号哲学家两边的筷子都可以用,但是却被阻塞
更准确的说法应该是:
-
各哲学家拿筷子这件事必须互斥的执行。
这就保证了即使一个哲学家在拿筷子拿到一半时被阻塞,也不会有别的哲学家会继续尝试拿筷子。
这样的话,当前正在吃饭的哲学家放下筷子后,被阻塞的哲学家就可以获得等待的筷子了。
Java 实现
class DiningPhilosophers {
public DiningPhilosophers() {
}
Lock[] chopstick = new ReentrantLock[]{
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock()
};
Lock mutex = new ReentrantLock();
// 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 {
//由于要同时持有,所以要互斥
mutex.lock();
chopstick[philosopher].lock();
chopstick[(philosopher + 1) % 5].lock();
mutex.unlock();
pickLeftFork.run();
pickRightFork.run();
//eat
eat.run();
putLeftFork.run();
putRightFork.run();
//放下左右筷子
chopstick[philosopher].unlock();
chopstick[(philosopher + 1) % 5].unlock();
}
}
3、位运算 + CAS 优化
方式① 最多允许四个哲学家同时进餐
/**
* 最多允许四个哲学家同时进餐
*/
class DiningPhilosophers {
public DiningPhilosophers() {
}
//若 chopstick 等于 0, 表示筷子未被使用
AtomicInteger chopstick = new AtomicInteger();
// 00001, 00010, 00100, 01000, 10000
int[] chopstickMask = new int[]{1, 2, 4, 8, 16};
Semaphore mutex = new Semaphore(4);
// 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 leftC = chopstickMask[philosopher];
int rightC = chopstickMask[(philosopher + 1) % 5];
//信号量 -1
mutex.acquire();
//尝试拿起左右筷子
//CAS
while (!get(leftC)) {
Thread.sleep(1);
}
while (!get(rightC)) {
Thread.sleep(1);
}
pickLeftFork.run();
pickRightFork.run();
//eat
eat.run();
putLeftFork.run();
putRightFork.run();
//放下左右筷子
while (!put(leftC)) {
Thread.sleep(1);
}
while (!put(rightC)) {
Thread.sleep(1);
}
//信号量 + 1
mutex.release();
}
private boolean put(int mask) {
int expect = chopstick.get();
//例如:1 号放下筷子 00001 和 00010
// 左边的筷子 expect : 00011 ^ 00001 -> 00010
// 右边的筷子 expect : 00010 ^ 00010 -> 00000
return chopstick.compareAndSet(expect, expect ^ mask);
}
//尝试拿起左右筷子
private boolean get(int mask) {
int expect = chopstick.get();
//只有当前筷子没有被拿起才返回 true, 并且更新筷子状态
// 例如:1 号拿起筷子 00001 和 00010 , 更新后 : 00011 (说明1, 2号筷子被拿了)
// 此时 2 号想拿起左边的筷子 : 00010 , & 运算发现被拿了, 返回false
return (expect & mask) <= 0 && chopstick.compareAndSet(expect, expect ^ mask);
}
}
方式② 奇数号哲学家先左后右, 偶数号哲学家先右后左
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 奇数号哲学家先左后右, 偶数号哲学家先右后左
*/
class DiningPhilosophers {
public DiningPhilosophers() {
}
//若 chopstick 等于 0, 表示筷子未被使用
AtomicInteger chopstick = new AtomicInteger();
// 00001, 00010, 00100, 01000, 10000
int[] chopstickMask = new int[]{1, 2, 4, 8, 16};
// 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 leftC = chopstickMask[philosopher];
int rightC = chopstickMask[(philosopher + 1) % 5];
//信号量 -1
if ((philosopher & 1) != 0) {
//尝试拿起左右筷子
while (!get(leftC)) {
Thread.sleep(1);
}
while (!get(rightC)) {
Thread.sleep(1);
}
} else {
//尝试拿起右左筷子
while (!get(rightC)) {
Thread.sleep(1);
}
while (!get(leftC)) {
Thread.sleep(1);
}
}
//退出临界区
pickLeftFork.run();
pickRightFork.run();
//eat
eat.run();
putLeftFork.run();
putRightFork.run();
//放下左右筷子
while (!put(leftC)) {
Thread.sleep(1);
}
while (!put(rightC)) {
Thread.sleep(1);
}
//信号量 + 1
}
private boolean put(int mask) {
int expect = chopstick.get();
//例如:1 号放下筷子 00001 和 00010
// 左边的筷子 expect : 00011 ^ 00001 -> 00010
// 右边的筷子 expect : 00010 ^ 00010 -> 00000
return chopstick.compareAndSet(expect, expect ^ mask);
}
//尝试拿起左右筷子
private boolean get(int mask) {
int expect = chopstick.get();
//只有当前筷子没有被拿起才返回 true, 并且更新筷子状态
// 例如:1 号拿起筷子 00001 和 00010 , 更新后 : 00011 (说明1, 2号筷子被拿了)
// 此时 2 号想拿起左边的筷子 : 00010 , & 运算发现被拿了, 返回false
return (expect & mask) <= 0 && chopstick.compareAndSet(expect, expect ^ mask);
}
}
方式③ 仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子。
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子。
*/
class DiningPhilosophers {
public DiningPhilosophers() {
}
//若 chopstick 等于 0, 表示筷子未被使用
AtomicInteger chopstick = new AtomicInteger();
// 00001, 00010, 00100, 01000, 10000
int[] chopstickMask = new int[]{1, 2, 4, 8, 16};
//进入临界区
AtomicInteger mutex = new AtomicInteger();
// 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 leftC = chopstickMask[philosopher];
int rightC = chopstickMask[(philosopher + 1) % 5];
//信号量 -1
//进入临界区
while (!mutex.compareAndSet(0, 1)) {
Thread.sleep(1);
}
//尝试拿起左右筷子
while (!get(leftC)) {
Thread.sleep(1);
}
while (!get(rightC)) {
Thread.sleep(1);
}
//退出临界区
while (!mutex.compareAndSet(1, 0)) {
Thread.sleep(1);
}
pickLeftFork.run();
pickRightFork.run();
//eat
eat.run();
putLeftFork.run();
putRightFork.run();
//放下左右筷子
while (!put(leftC)) {
Thread.sleep(1);
}
while (!put(rightC)) {
Thread.sleep(1);
}
//信号量 + 1
}
private boolean put(int mask) {
int expect = chopstick.get();
//例如:1 号放下筷子 00001 和 00010
// 左边的筷子 expect : 00011 ^ 00001 -> 00010
// 右边的筷子 expect : 00010 ^ 00010 -> 00000
return chopstick.compareAndSet(expect, expect ^ mask);
}
//尝试拿起左右筷子
private boolean get(int mask) {
int expect = chopstick.get();
//只有当前筷子没有被拿起才返回 true, 并且更新筷子状态
// 例如:1 号拿起筷子 00001 和 00010 , 更新后 : 00011 (说明1, 2号筷子被拿了)
// 此时 2 号想拿起左边的筷子 : 00010 , & 运算发现被拿了, 返回false
return (expect & mask) <= 0 && chopstick.compareAndSet(expect, expect ^ mask);
}
}
4、总结
哲学家进餐问题的关键在于解决进程死锁。
这些进程之间只存在互斥关系,但是与之前接触到的互斥关系不同的是,每个进程都需要同时持有两个临界资源,因此就有“死锁”问题的隐患。
如果在考试中遇到了一个进程需要同时持有多个临界资源的情况,应该参考哲学家问题的思想,分析题中给出的进程之间是否会发生循环等待,是否会发生死锁。
可以参考哲学家就餐问题解决死锁的三种思路。