哲学家就餐问题
哲学家就餐问题 (Dining Philosophers Problem)
1. 问题概述
哲学家就餐问题是一个经典的并发控制问题,由荷兰计算机科学家Edsger W. Dijkstra于1965年提出。这个问题用于演示如何避免死锁和资源争用的并发系统设计挑战。
问题描述:
- 有5位哲学家围坐在一张圆桌旁
- 每位哲学家之间有一把叉子(共5把叉子)
- 哲学家的生活只有两种状态:思考和吃饭
- 哲学家需要同时拿起左右两边的叉子才能吃饭
- 吃完饭后,哲学家会放下叉子,继续思考
2. 问题模型
2.1 基本要素
- 哲学家:并发执行的进程或线程
- 叉子:共享资源,代表临界区或锁
- 状态转换:
- 思考 → 饿了 → 尝试获取叉子 → 吃饭 → 放下叉子 → 思考
2.2 图示模型
2.3 问题本质
哲学家就餐问题本质上是资源分配问题和进程同步问题的结合:
- 每个哲学家需要两个资源(两把叉子)才能继续执行
- 资源获取顺序不当可能导致死锁
- 缺乏适当的同步机制会导致资源争用和饥饿
3. 死锁风险
如果每位哲学家同时拿起左边的叉子,然后都试图拿起右边的叉子,就会发生死锁:
哲学家0拿起叉子0,等待叉子1
哲学家1拿起叉子1,等待叉子0
哲学家2拿起叉子2,等待叉子3
哲学家3拿起叉子3,等待叉子4
哲学家4拿起叉子4,等待叉子0在这种情况下,所有哲学家都在等待对方释放叉子,形成循环等待,导致死锁。
4. 解决方案
4.1 解决方案一:最多允许4位哲学家同时就餐
思路
限制同时拿起叉子的哲学家数量,确保至少有一位哲学家能够完成用餐并释放资源。
实现原理
- 使用一个计数器记录当前正在尝试获取叉子的哲学家数量
- 当计数器达到4时,第5位哲学家必须等待
- 这样至少有一位哲学家能够获得左右两把叉子,吃完后释放资源
Java实现
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;
public class DiningPhilosophersWithLimit {
private static final int NUM_PHILOSOPHERS = 5;
private final Lock[] forks = new Lock[NUM_PHILOSOPHERS]; // 叉子锁
private final Semaphore maxDiners; // 限制同时就餐的哲学家数量
private final Random random = new Random();
public DiningPhilosophersWithLimit() {
// 初始化叉子锁
for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
forks[i] = new ReentrantLock();
}
// 最多允许4个哲学家同时尝试获取叉子
maxDiners = new Semaphore(4);
}
// 哲学家行为
private void philosopher(int id) throws InterruptedException {
while (true) {
think(id);
eat(id);
}
}
// 思考
private void think(int id) throws InterruptedException {
System.out.println("哲学家 " + id + " 正在思考");
TimeUnit.MILLISECONDS.sleep(random.nextInt(1000));
}
// 吃饭
private void eat(int id) throws InterruptedException {
// 尝试获取许可,限制同时就餐的哲学家数量
maxDiners.acquire();
try {
// 获取左右叉子(编号较小的叉子优先)
int leftFork = id;
int rightFork = (id + 1) % NUM_PHILOSOPHERS;
// 总是先拿编号较小的叉子
if (leftFork > rightFork) {
int temp = leftFork;
leftFork = rightFork;
rightFork = temp;
}
// 获取左边叉子
forks[leftFork].lock();
System.out.println("哲学家 " + id + " 拿起了叉子 " + leftFork);
// 获取右边叉子
forks[rightFork].lock();
System.out.println("哲学家 " + id + " 拿起了叉子 " + rightFork);
// 吃饭
System.out.println("哲学家 " + id + " 正在吃饭");
TimeUnit.MILLISECONDS.sleep(random.nextInt(1000));
} finally {
// 放下叉子
int leftFork = id;
int rightFork = (id + 1) % NUM_PHILOSOPHERS;
forks[leftFork].unlock();
forks[rightFork].unlock();
System.out.println("哲学家 " + id + " 放下了叉子 " + leftFork + " 和 " + rightFork);
// 释放许可
maxDiners.release();
}
}
public static void main(String[] args) {
DiningPhilosophersWithLimit dining = new DiningPhilosophersWithLimit();
// 创建并启动哲学家线程
for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
final int id = i;
Thread thread = new Thread(() -> {
try {
dining.philosopher(id);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
thread.start();
}
}
}4.2 解决方案二:资源分级获取
思路
通过规定哲学家获取叉子的顺序,避免循环等待条件。
实现原理
通过为叉子编号并规定哲学家必须按编号顺序获取资源(先小后大),打破了循环等待条件。这种方法确保不会所有哲学家同时持有低编号叉子并等待高编号叉子,从而避免死锁。
不存在"互相等待对方叉子"的循环
假设所有哲学家同时想吃饭,都会先尝试拿"编号小的叉子":
- 哲学家0、1、2、3会先拿0、1、2、3号叉子(各自的左叉,且是小编号)
- 哲学家4会先拿0号叉子(因为他的右叉是0,比左叉4小)
此时只会出现"竞争同一把小编号叉子"的情况(比如0号和4号哲学家都抢0号叉子),但抢到的人才能继续拿第二把,没抢到的人会阻塞,不会形成"你等我、我等你"的循环。
Java实现
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;
public class DiningPhilosophersWithOrderedResources {
private static final int NUM_PHILOSOPHERS = 5;
private final Lock[] forks = new Lock[NUM_PHILOSOPHERS];
private final Random random = new Random();
public DiningPhilosophersWithOrderedResources() {
// 初始化叉子锁
for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
forks[i] = new ReentrantLock();
}
}
// 哲学家行为
private void philosopher(int id) throws InterruptedException {
while (true) {
think(id);
eat(id);
}
}
// 思考
private void think(int id) throws InterruptedException {
System.out.println("哲学家 " + id + " 正在思考");
TimeUnit.MILLISECONDS.sleep(random.nextInt(1000));
}
// 吃饭
private void eat(int id) throws InterruptedException {
// 确定左右叉子编号
int leftFork = id;
int rightFork = (id + 1) % NUM_PHILOSOPHERS;
// 关键:总是先获取编号较小的叉子
int firstFork = Math.min(leftFork, rightFork);
int secondFork = Math.max(leftFork, rightFork);
// 尝试获取第一把叉子
forks[firstFork].lock();
System.out.println("哲学家 " + id + " 拿起了叉子 " + firstFork);
try {
// 尝试获取第二把叉子
forks[secondFork].lock();
System.out.println("哲学家 " + id + " 拿起了叉子 " + secondFork);
// 吃饭
System.out.println("哲学家 " + id + " 正在吃饭");
TimeUnit.MILLISECONDS.sleep(random.nextInt(1000));
} finally {
// 释放叉子,顺序无关紧要
forks[secondFork].unlock();
forks[firstFork].unlock();
System.out.println("哲学家 " + id + " 放下了叉子 " + firstFork + " 和 " + secondFork);
}
}
public static void main(String[] args) {
DiningPhilosophersWithOrderedResources dining = new DiningPhilosophersWithOrderedResources();
// 创建并启动哲学家线程
for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
final int id = i;
Thread thread = new Thread(() -> {
try {
dining.philosopher(id);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
thread.start();
}
}
}4.3 解决方案三:信号量实现
思路
使用信号量(Semaphore)来控制资源访问,结合互斥锁确保原子操作。
实现原理
- 使用一个信号量表示一把叉子
- 使用互斥锁确保哲学家获取两把叉子的操作是原子的
- 或者让一位哲学家与其他哲学家获取叉子的顺序相反
Java实现
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.Random;
public class DiningPhilosophersWithSemaphores {
private static final int NUM_PHILOSOPHERS = 5;
private final Semaphore[] forks = new Semaphore[NUM_PHILOSOPHERS];
private final Random random = new Random();
public DiningPhilosophersWithSemaphores() {
// 初始化叉子信号量,每个信号量初始化为1(表示资源可用)
for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
forks[i] = new Semaphore(1);
}
}
// 哲学家行为
private void philosopher(int id) throws InterruptedException {
while (true) {
think(id);
eat(id);
}
}
// 思考
private void think(int id) throws InterruptedException {
System.out.println("哲学家 " + id + " 正在思考");
TimeUnit.MILLISECONDS.sleep(random.nextInt(1000));
}
// 吃饭 - 对最后一位哲学家特殊处理,反转获取叉子的顺序
private void eat(int id) throws InterruptedException {
int leftFork = id;
int rightFork = (id + 1) % NUM_PHILOSOPHERS;
// 最后一位哲学家先拿右边的叉子,打破循环依赖
if (id == NUM_PHILOSOPHERS - 1) {
// 获取右边叉子
forks[rightFork].acquire();
System.out.println("哲学家 " + id + " 拿起了叉子 " + rightFork);
// 获取左边叉子
forks[leftFork].acquire();
System.out.println("哲学家 " + id + " 拿起了叉子 " + leftFork);
} else {
// 其他哲学家先拿左边的叉子
forks[leftFork].acquire();
System.out.println("哲学家 " + id + " 拿起了叉子 " + leftFork);
forks[rightFork].acquire();
System.out.println("哲学家 " + id + " 拿起了叉子 " + rightFork);
}
try {
// 吃饭
System.out.println("哲学家 " + id + " 正在吃饭");
TimeUnit.MILLISECONDS.sleep(random.nextInt(1000));
} finally {
// 释放叉子
forks[leftFork].release();
forks[rightFork].release();
System.out.println("哲学家 " + id + " 放下了叉子 " + leftFork + " 和 " + rightFork);
}
}
public static void main(String[] args) {
DiningPhilosophersWithSemaphores dining = new DiningPhilosophersWithSemaphores();
// 创建并启动哲学家线程
for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
final int id = i;
Thread thread = new Thread(() -> {
try {
dining.philosopher(id);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
thread.start();
}
}
}4.4 解决方案四:使用Condition实现饥饿避免
思路
使用显式锁(Lock)和条件变量(Condition)实现更精细的控制,避免饥饿问题。
实现原理
- 跟踪每个哲学家和叉子的状态
- 使用条件变量让哲学家在无法获取叉子时等待
- 实现公平的资源分配策略
Java实现
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;
public class DiningPhilosophersWithConditions {
private static final int NUM_PHILOSOPHERS = 5;
// 哲学家状态枚举
private enum State {
THINKING, HUNGRY, EATING
}
private final State[] states = new State[NUM_PHILOSOPHERS]; // 哲学家状态
private final Lock lock = new ReentrantLock(); // 全局锁
private final Condition[] self = new Condition[NUM_PHILOSOPHERS]; // 每个哲学家的条件变量
private final Random random = new Random();
public DiningPhilosophersWithConditions() {
// 初始化所有哲学家为思考状态
for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
states[i] = State.THINKING;
self[i] = lock.newCondition();
}
}
// 检查哲学家是否可以开始吃饭
private void test(int id) {
// 如果当前哲学家饥饿,且左右邻居都不在吃饭,则可以开始吃饭
if (states[id] == State.HUNGRY &&
states[(id + NUM_PHILOSOPHERS - 1) % NUM_PHILOSOPHERS] != State.EATING &&
states[(id + 1) % NUM_PHILOSOPHERS] != State.EATING) {
states[id] = State.EATING;
self[id].signal(); // 唤醒等待的哲学家
}
}
// 获取叉子
private void takeForks(int id) throws InterruptedException {
lock.lock();
try {
states[id] = State.HUNGRY;
System.out.println("哲学家 " + id + " 饿了,正在等待叉子");
test(id); // 尝试获取叉子
if (states[id] != State.EATING) {
self[id].await(); // 如果无法获取叉子,则等待
}
System.out.println("哲学家 " + id + " 获得了两把叉子,可以开始吃饭");
} finally {
lock.unlock();
}
}
// 放下叉子
private void putForks(int id) {
lock.lock();
try {
states[id] = State.THINKING;
System.out.println("哲学家 " + id + " 放下了叉子,继续思考");
// 测试左右邻居是否可以开始吃饭
test((id + NUM_PHILOSOPHERS - 1) % NUM_PHILOSOPHERS);
test((id + 1) % NUM_PHILOSOPHERS);
} finally {
lock.unlock();
}
}
// 哲学家行为
private void philosopher(int id) throws InterruptedException {
while (true) {
think(id);
takeForks(id);
eat(id);
putForks(id);
}
}
// 思考
private void think(int id) throws InterruptedException {
System.out.println("哲学家 " + id + " 正在思考");
TimeUnit.MILLISECONDS.sleep(random.nextInt(1000));
}
// 吃饭
private void eat(int id) throws InterruptedException {
System.out.println("哲学家 " + id + " 正在吃饭");
TimeUnit.MILLISECONDS.sleep(random.nextInt(1000));
}
public static void main(String[] args) {
DiningPhilosophersWithConditions dining = new DiningPhilosophersWithConditions();
// 创建并启动哲学家线程
for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
final int id = i;
Thread thread = new Thread(() -> {
try {
dining.philosopher(id);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
thread.start();
}
}
}5. 各种解决方案的比较
| 解决方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 限制同时就餐人数 | 实现简单,效果好 | 资源利用率不高 | 资源竞争不激烈的场景 |
| 资源分级获取 | 避免死锁,实现简单 | 某些哲学家可能等待时间较长 | 需要避免死锁的场景 |
| 信号量实现 | 灵活性高,易于扩展 | 需要额外的同步机制 | 复杂的并发控制场景 |
| 条件变量实现 | 避免饥饿,公平性好 | 实现复杂 | 对公平性要求高的场景 |
6. 高级实现:带优先级和饥饿检测的哲学家就餐问题
下面是一个更高级的实现,包含哲学家优先级、饥饿检测和资源分配优化:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;
import java.util.PriorityQueue;
import java.util.Comparator;
public class AdvancedDiningPhilosophers {
private static final int NUM_PHILOSOPHERS = 5;
private static final int HUNGER_THRESHOLD = 3000; // 饥饿阈值(毫秒)
// 哲学家状态枚举
private enum State {
THINKING, HUNGRY, EATING
}
// 哲学家类
private class Philosopher implements Comparable<Philosopher> {
private final int id;
private State state;
private long hungerStartTime; // 开始饥饿的时间
private int priority; // 优先级,数字越小优先级越高
private final Condition condition;
private int eatCount; // 吃饭次数
public Philosopher(int id) {
this.id = id;
this.state = State.THINKING;
this.priority = id; // 初始优先级基于ID
this.condition = lock.newCondition();
this.eatCount = 0;
}
@Override
public int compareTo(Philosopher other) {
// 优先级比较
return Integer.compare(this.priority, other.priority);
}
@Override
public String toString() {
return "哲学家" + id + "[状态:" + state + ",优先级:" + priority + ",吃饭次数:" + eatCount + "]";
}
}
private final Philosopher[] philosophers = new Philosopher[NUM_PHILOSOPHERS];
private final Lock lock = new ReentrantLock();
private final Random random = new Random();
private final PriorityQueue<Philosopher> hungryQueue = new PriorityQueue<>();
private final AtomicInteger systemTime = new AtomicInteger(0); // 模拟系统时间
public AdvancedDiningPhilosophers() {
// 初始化哲学家
for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
philosophers[i] = new Philosopher(i);
}
// 启动饥饿检测线程
Thread hungerDetector = new Thread(this::detectHunger);
hungerDetector.setDaemon(true);
hungerDetector.start();
// 启动系统时间模拟线程
Thread timeSimulator = new Thread(() -> {
while (true) {
try {
TimeUnit.MILLISECONDS.sleep(100);
systemTime.incrementAndGet();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
timeSimulator.setDaemon(true);
timeSimulator.start();
}
// 饥饿检测
private void detectHunger() {
while (true) {
try {
TimeUnit.MILLISECONDS.sleep(500);
lock.lock();
long currentTime = systemTime.get();
for (Philosopher p : philosophers) {
if (p.state == State.HUNGRY) {
long hungerTime = currentTime - p.hungerStartTime;
if (hungerTime > HUNGER_THRESHOLD) {
// 提高饥饿哲学家的优先级
p.priority = Math.max(0, p.priority - 1);
System.out.println("警告: " + p + " 已经饥饿 " + hungerTime + " 毫秒,提高优先级到 " + p.priority);
// 重新排序饥饿队列
rebuildHungryQueue();
// 尝试唤醒饥饿的哲学家
for (Philosopher philosopher : philosophers) {
test(philosopher.id);
}
}
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
// 重建饥饿队列
private void rebuildHungryQueue() {
hungryQueue.clear();
for (Philosopher p : philosophers) {
if (p.state == State.HUNGRY) {
hungryQueue.offer(p);
}
}
}
// 测试哲学家是否可以吃饭
private boolean test(int id) {
Philosopher p = philosophers[id];
Philosopher left = philosophers[(id + NUM_PHILOSOPHERS - 1) % NUM_PHILOSOPHERS];
Philosopher right = philosophers[(id + 1) % NUM_PHILOSOPHERS];
if (p.state == State.HUNGRY && left.state != State.EATING && right.state != State.EATING) {
p.state = State.EATING;
p.eatCount++;
p.condition.signal();
System.out.println(p + " 开始吃饭");
return true;
}
return false;
}
// 获取叉子
private void takeForks(int id) throws InterruptedException {
lock.lock();
try {
Philosopher p = philosophers[id];
p.state = State.HUNGRY;
p.hungerStartTime = systemTime.get();
hungryQueue.offer(p);
System.out.println(p + " 饿了,进入等待队列");
// 按优先级处理饥饿的哲学家
if (!test(id)) {
p.condition.await();
}
} finally {
lock.unlock();
}
}
// 放下叉子
private void putForks(int id) {
lock.lock();
try {
Philosopher p = philosophers[id];
p.state = State.THINKING;
System.out.println(p + " 放下叉子");
// 恢复默认优先级,但保持一定的历史影响
p.priority = Math.min(NUM_PHILOSOPHERS, p.priority + 1);
// 重建饥饿队列
rebuildHungryQueue();
// 测试左右邻居,同时考虑优先级队列中的哲学家
test((id + NUM_PHILOSOPHERS - 1) % NUM_PHILOSOPHERS);
test((id + 1) % NUM_PHILOSOPHERS);
// 尝试唤醒优先级最高的饥饿哲学家
if (!hungryQueue.isEmpty()) {
Philosopher next = hungryQueue.peek();
test(next.id);
}
} finally {
lock.unlock();
}
}
// 哲学家行为
private void philosopherBehavior(int id) throws InterruptedException {
while (true) {
think(id);
takeForks(id);
eat(id);
putForks(id);
}
}
// 思考
private void think(int id) throws InterruptedException {
System.out.println(philosophers[id] + " 正在思考");
TimeUnit.MILLISECONDS.sleep(500 + random.nextInt(1000));
}
// 吃饭
private void eat(int id) throws InterruptedException {
System.out.println(philosophers[id] + " 正在吃饭");
TimeUnit.MILLISECONDS.sleep(300 + random.nextInt(700));
}
// 显示系统状态
private void displaySystemStatus() {
lock.lock();
try {
System.out.println("\n===== 系统状态 =====");
System.out.println("饥饿队列: " + hungryQueue);
System.out.println("哲学家状态:");
for (Philosopher p : philosophers) {
System.out.println(" " + p);
}
System.out.println("====================\n");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
AdvancedDiningPhilosophers dining = new AdvancedDiningPhilosophers();
// 创建并启动状态显示线程
Thread statusThread = new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(2);
dining.displaySystemStatus();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
statusThread.setDaemon(true);
statusThread.start();
// 创建并启动哲学家线程
for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
final int id = i;
Thread thread = new Thread(() -> {
try {
dining.philosopherBehavior(id);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
thread.start();
}
}
}7. 哲学家就餐问题的实际应用
7.1 数据库事务调度
在数据库系统中,多个事务同时访问共享数据可能导致死锁,哲学家就餐问题的解决方案可以用于设计事务调度算法。
7.2 资源分配系统
操作系统中的资源分配器需要处理多个进程对共享资源的竞争,哲学家就餐问题的思想可以应用于资源分配策略设计。
7.3 分布式系统中的协调
在分布式系统中,节点之间的资源协调可以借鉴哲学家就餐问题的解决方案。
7.4 多线程编程中的锁设计
在多线程编程中,避免死锁和饥饿是重要的设计考虑,哲学家就餐问题提供了宝贵的参考。
8. 代码优化与性能考虑
8.1 减少锁竞争
- 使用细粒度锁,只在必要时锁定资源
- 实现非阻塞算法,减少线程阻塞时间
8.2 公平性与饥饿避免
- 实现优先级调度,确保长时间等待的线程有更高优先级
- 使用轮换策略,确保每个线程都有机会获取资源
8.3 性能调优
- 调整超时时间和重试策略
- 使用异步处理和非阻塞IO
- 考虑使用无锁数据结构
9. 总结
哲学家就餐问题是一个经典的并发控制问题,它揭示了多线程编程中的死锁、资源竞争和饥饿等重要概念。通过学习和实现不同的解决方案,我们可以深入理解并发编程中的核心挑战和解决策略。
在实际应用中,我们需要根据具体场景选择合适的并发控制方法,通常会结合多种技术来确保系统的正确性、可靠性和性能。哲学家就餐问题的思想不仅适用于操作系统和分布式系统的设计,也为现代多线程编程提供了宝贵的指导。