用 Java 复现哲学家就餐问题

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


1 问题背景

  • 有 5 个哲学家围坐在圆桌旁,每人左右各有一根筷子。
  • 哲学家需要 两根筷子 才能进餐。
  • 每个哲学家有两个动作:思考 (think) 和进餐 (eat)。

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


2 死锁场景复现

Java 实现(死锁版本)

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();
        }
    }
}

死锁现象

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

plain 复制代码
哲学家 0 拿起左手筷子,等待右手筷子...
哲学家 1 拿起左手筷子,等待右手筷子...
哲学家 2 拿起左手筷子,等待右手筷子...
哲学家 3 拿起左手筷子,等待右手筷子...
哲学家 4 拿起左手筷子,等待右手筷子...

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


3 解决办法

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


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

思路

  • 使用一个"服务员"信号量,最多允许 N-1 个哲学家同时拿筷子。
  • 这样就不会出现所有人都占用一根筷子的情况。

Java 实现

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 实现

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. 奇偶编号方案:通过调整拿筷子顺序打破环路。

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

相关推荐
大江东去浪淘尽千古风流人物几秒前
【SLAM新范式】几何主导=》几何+学习+语义+高效表示的融合
深度学习·算法·slam
重生之我是Java开发战士14 分钟前
【优选算法】模拟算法:替换所有的问号,提莫攻击,N字形变换,外观数列,数青蛙
算法
仟濹20 分钟前
算法打卡 day1 (2026-02-06 周四) | 算法: DFS | 1_卡码网98 可达路径 | 2_力扣797_所有可能的路径
算法·leetcode·深度优先
yang)21 分钟前
欠采样时的相位倒置问题
算法
历程里程碑24 分钟前
Linux20 : IO
linux·c语言·开发语言·数据结构·c++·算法
A尘埃26 分钟前
物流公司配送路径动态优化(Q-Learning算法)
算法
天若有情67327 分钟前
【自研实战】轻量级ASCII字符串加密算法:从设计到落地(防查岗神器版)
网络·c++·算法·安全·数据安全·加密
啊森要自信1 小时前
CANN ops-cv:AI 硬件端视觉算法推理训练的算子性能调优与实战应用详解
人工智能·算法·cann
仟濹1 小时前
算法打卡day2 (2026-02-07 周五) | 算法: DFS | 3_卡码网99_计数孤岛_DFS
算法·深度优先
驭渊的小故事1 小时前
简单模板笔记
数据结构·笔记·算法