TANKENQI.cn

August 20, 2025

用 Java 复现哲学家就餐问题

OS5.9 min to read

哲学家就餐问题(Dining Philosophers Problem)是计算机科学中一个经典的并发控制问题,常用来解释多线程环境下的同步死锁问题。本文将结合 Java 的 Semaphore(信号量)机制,先演示死锁场景,再介绍两种常见的解决方案。


1 问题背景

如果所有哲学家都先拿左手边的筷子,再尝试拿右手边的筷子,就可能出现所有人都拿了一根筷子并等待另一根筷子的情况,导致整个系统进入死锁。

哲学家就餐的问题


2 死锁场景复现

Java 实现(死锁版本)

import java.util.concurrent.Semaphore;class Philosopher extends Thread {    private int id;    private Semaphore[] chopsticks;    public Philosopher(int id, Semaphore[] chopsticks) {        this.id = id;        this.chopsticks = chopsticks;    }    private void think() throws InterruptedException {        System.out.println("哲学家 " + id + " 正在思考...");        Thread.sleep((int) (Math.random() * 1000));    }    private void eat() throws InterruptedException {        System.out.println("哲学家 " + id + " 正在吃饭...");        Thread.sleep((int) (Math.random() * 1000));    }    @Override    public void run() {        try {            while (true) {                think();                // 所有人都先拿左手边的筷子                chopsticks[id].acquire();                chopsticks[(id + 1) % chopsticks.length].acquire();                eat();                chopsticks[id].release();                chopsticks[(id + 1) % chopsticks.length].release();            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}public class DiningPhilosophersDeadlock {    public static void main(String[] args) {        int N = 5;        Semaphore[] chopsticks = new Semaphore[N];        for (int i = 0; i < N; i++) chopsticks[i] = new Semaphore(1);        Philosopher[] philosophers = new Philosopher[N];        for (int i = 0; i < N; i++) {            philosophers[i] = new Philosopher(i, chopsticks);            philosophers[i].start();        }    }}

死锁现象

运行一段时间后,可能出现以下情况:

哲学家 0 拿起左手筷子,等待右手筷子...哲学家 1 拿起左手筷子,等待右手筷子...哲学家 2 拿起左手筷子,等待右手筷子...哲学家 3 拿起左手筷子,等待右手筷子...哲学家 4 拿起左手筷子,等待右手筷子...

所有哲学家都拿了一根筷子,互相等待,导致死锁。


3 解决办法

哲学家就餐问题的核心挑战是:如何避免死锁,同时尽量减少等待。下面介绍两种常见的解决方案。


方法一:服务员方案(限制同时拿筷子人数)

思路

Java 实现

import java.util.concurrent.Semaphore;class Philosopher extends Thread {    private int id;    private Semaphore[] chopsticks;    private Semaphore waiter;    public Philosopher(int id, Semaphore[] chopsticks, Semaphore waiter) {        this.id = id;        this.chopsticks = chopsticks;        this.waiter = waiter;    }    private void think() throws InterruptedException {        System.out.println("哲学家 " + id + " 正在思考...");        Thread.sleep((int) (Math.random() * 1000));    }    private void eat() throws InterruptedException {        System.out.println("哲学家 " + id + " 正在吃饭...");        Thread.sleep((int) (Math.random() * 1000));    }    @Override    public void run() {        try {            while (true) {                think();                waiter.acquire();                chopsticks[id].acquire();                chopsticks[(id + 1) % chopsticks.length].acquire();                eat();                chopsticks[id].release();                chopsticks[(id + 1) % chopsticks.length].release();                waiter.release();            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}public class DiningPhilosophersWaiter {    public static void main(String[] args) {        int N = 5;        Semaphore[] chopsticks = new Semaphore[N];        for (int i = 0; i < N; i++) chopsticks[i] = new Semaphore(1);        Semaphore waiter = new Semaphore(N - 1);        Philosopher[] philosophers = new Philosopher[N];        for (int i = 0; i < N; i++) {            philosophers[i] = new Philosopher(i, chopsticks, waiter);            philosophers[i].start();        }    }}

特点

✅ 简单可靠,避免死锁。
❌ 引入了额外的“服务员”角色。


方法二:奇偶编号方案(打破环路)

思路

Java 实现

import java.util.concurrent.Semaphore;class Philosopher extends Thread {    private int id;    private Semaphore[] chopsticks;    public Philosopher(int id, Semaphore[] chopsticks) {        this.id = id;        this.chopsticks = chopsticks;    }    private void think() throws InterruptedException {        System.out.println("哲学家 " + id + " 正在思考...");        Thread.sleep((int) (Math.random() * 1000));    }    private void eat() throws InterruptedException {        System.out.println("哲学家 " + id + " 正在吃饭...");        Thread.sleep((int) (Math.random() * 1000));    }    @Override    public void run() {        try {            while (true) {                think();                if (id % 2 == 0) {                    chopsticks[id].acquire();                    chopsticks[(id + 1) % chopsticks.length].acquire();                } else {                    chopsticks[(id + 1) % chopsticks.length].acquire();                    chopsticks[id].acquire();                }                eat();                chopsticks[id].release();                chopsticks[(id + 1) % chopsticks.length].release();            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}public class DiningPhilosophersOddEven {    public static void main(String[] args) {        int N = 5;        Semaphore[] chopsticks = new Semaphore[N];        for (int i = 0; i < N; i++) chopsticks[i] = new Semaphore(1);        Philosopher[] philosophers = new Philosopher[N];        for (int i = 0; i < N; i++) {            philosophers[i] = new Philosopher(i, chopsticks);            philosophers[i].start();        }    }}

特点

✅ 不需要额外的“服务员”,逻辑更简洁。
❌ 可能存在哲学家饥饿(长时间没机会进餐)的情况。


4 总结

哲学家就餐问题反映了多线程编程中的核心挑战:

本文给出了:

  1. 死锁场景(所有人先拿左手筷子)。
  2. 服务员方案:用信号量限制并发度,避免死锁。
  3. 奇偶编号方案:通过调整拿筷子顺序打破环路。

👉 这三种实现很好地展示了操作系统课本中“死锁条件”与“死锁避免”的思想。