LeetCode 1226. 哲学家进餐
题目描述



题意分析
五位哲学家共享五把叉子,每人只有同时占用左右两把才能进餐,同一把叉子不能被两人同时使用。题目给出的拿叉、进餐、放叉函数只是回调,需要我们安排它们在并发调用中的执行时机。
只保证每把叉子互斥还不够:如果所有人都先占用左叉,再等待右叉,就可能每人持有一把、谁也无法继续。还必须规定一致的资源申请顺序。
解法:统一叉子编号加锁
核心思路
[!blue]
给五把共享叉子各分配一把锁。代码把编号为
p的哲学家的左右叉分别记为p和(p + 1) % 5,相邻哲学家因此会访问同一把叉子的同一个锁。每次调用先锁两把叉子中编号较小的一把,再锁较大的一把。等待第一把锁时,线程还没有占用叉子;持有第一把再等待第二把时,只可能从较小编号等待较大编号。若发生循环等待,就要求编号沿环严格递增后又回到起点,这是不可能的,所以不会死锁。
得到两把锁以后,才依次调用拿左叉、拿右叉、进餐、放左叉、放右叉。加锁顺序按资源编号决定,回调名称仍表示哲学家的左右方向,不能因为交换了加锁顺序就混淆回调含义。
放叉回调执行完之前一直持有锁,防止相邻哲学家提前使用同一把叉子。Java 用
finally、Go 用defer释放锁;不共享叉子的两个人仍可以并行进餐。
解题步骤
- 在共享对象中初始化五把叉子锁,所有调用复用这些锁。
- 根据哲学家编号计算左右叉,再得到
firstFork和secondFork,保证前者编号更小。- 依次获取两把锁,全部取得后执行两次拿叉、一次进餐和两次放叉回调。
- 通过可靠的退出路径释放两把锁,允许其他等待者继续申请;Go 的
defer按后登记先执行的顺序解锁。
代码实现
class DiningPhilosophers {
private final java.util.concurrent.locks.ReentrantLock[] forks;
public DiningPhilosophers() {
forks = new java.util.concurrent.locks.ReentrantLock[5];
for (int i = 0; i < forks.length; i++) {
forks[i] = new java.util.concurrent.locks.ReentrantLock();
}
}
public void wantsToEat(
int philosopher,
Runnable pickLeftFork,
Runnable pickRightFork,
Runnable eat,
Runnable putLeftFork,
Runnable putRightFork)
throws InterruptedException {
int leftFork = philosopher;
int rightFork = (philosopher + 1) % forks.length;
int firstFork = Math.min(leftFork, rightFork);
int secondFork = Math.max(leftFork, rightFork);
// 所有人按同一资源编号顺序申请,等待关系不能成环。
forks[firstFork].lock();
forks[secondFork].lock();
try {
pickLeftFork.run();
pickRightFork.run();
eat.run();
putLeftFork.run();
putRightFork.run();
} finally {
// 回调结束或异常离开时都释放已取得的锁。
forks[secondFork].unlock();
forks[firstFork].unlock();
}
}
}
import "sync"
type DiningPhilosophers struct {
forks [5]sync.Mutex
}
func Constructor() DiningPhilosophers {
return DiningPhilosophers{}
}
func (d *DiningPhilosophers) WantsToEat(
philosopher int,
pickLeftFork func(),
pickRightFork func(),
eat func(),
putLeftFork func(),
putRightFork func(),
) {
leftFork := philosopher
rightFork := (philosopher + 1) % len(d.forks)
firstFork := leftFork
secondFork := rightFork
if firstFork > secondFork {
firstFork, secondFork = secondFork, firstFork
}
// 所有人按同一资源编号顺序申请,等待关系不能成环。
d.forks[firstFork].Lock()
d.forks[secondFork].Lock()
// 登记释放动作,后登记的较大编号锁先释放。
defer d.forks[firstFork].Unlock()
defer d.forks[secondFork].Unlock()
pickLeftFork()
pickRightFork()
eat()
putLeftFork()
putRightFork()
}
复杂度分析
- 时间复杂度:单次自身同步工作 $O(1)$,实际等待与回调耗时另计。
- 空间复杂度:固定五把锁及每次调用的常数局部状态。
关键点总结
[!green]
- 统一资源顺序防止死锁,不等同于保证每个人的等待时间有上界。
- 互斥保护的是叉子,不是哲学家编号。
易错点总结
[!yellow]
- 每人固定先拿左叉,五人可能形成环形等待。
- 只锁一把叉子就进餐,相邻两人可能同时使用另一把。
- 回调后没有可靠释放路径,会在异常退出时留下锁。
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!