惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - 小纸条

ruoyiai 启动指南 反向传播 numpy的使用 B 和 B+树 红黑树 ruoyi-vue 梯度下降法 博弈论 离散化 AcWing 907. 区间覆盖 AcWing 906. 区间分组 AcWing 908 最大不相交区间数量 AcWing 905. 区间选点 AcWing 104. 货仓选址 动态规划经典题 窗口函数 1195. 交替打印字符串 1117. H2O 生成 1116. 打印零与奇偶数 关联子查询
1226. 哲学家进餐
小纸条 · 2025-10-28 · via 博客园 - 小纸条

1226. 哲学家进餐

题目描述

5 个沉默寡言的哲学家围坐在圆桌前,每人面前一盘意面。叉子放在哲学家之间的桌面上。(5 个哲学家,5 根叉子)

所有的哲学家都只会在思考和进餐两种行为间交替。哲学家只有同时拿到左边和右边的叉子才能吃到面,而同一根叉子在同一时间只能被一个哲学家使用。每个哲学家吃完面后都需要把叉子放回桌面以供其他哲学家吃面。只要条件允许,哲学家可以拿起左边或者右边的叉子,但在没有同时拿到左右叉子时不能进食。

假设面的数量没有限制,哲学家也能随便吃,不需要考虑吃不吃得下。

设计一个进餐规则(并行算法)使得每个哲学家都不会挨饿;也就是说,在没有人知道别人什么时候想吃东西或思考的情况下,每个哲学家都可以在吃饭和思考之间一直交替下去。

问题描述和图片来自维基百科 wikipedia.org

哲学家从 04顺时针 编号。请实现函数 void wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork)

  • philosopher 哲学家的编号。
  • pickLeftFork 和 pickRightFork 表示拿起左边或右边的叉子。
  • eat 表示吃面。
  • putLeftFork 和 putRightFork 表示放下左边或右边的叉子。
  • 由于哲学家不是在吃面就是在想着啥时候吃面,所以思考这个方法没有对应的回调。

给你 5 个线程,每个都代表一个哲学家,请你使用类的同一个对象来模拟这个过程。在最后一次调用结束之前,可能会为同一个哲学家多次调用该函数。

示例:

输入:n = 1
输出:[[4,2,1],[4,1,1],[0,1,1],[2,2,1],[2,1,1],[2,0,3],[2,1,2],[2,2,2],[4,0,3],[4,1,2],[0,2,1],[4,2,2],[3,2,1],[3,1,1],[0,0,3],[0,1,2],[0,2,2],[1,2,1],[1,1,1],[3,0,3],[3,1,2],[3,2,2],[1,0,3],[1,1,2],[1,2,2]]
解释:
n 表示每个哲学家需要进餐的次数。
输出数组描述了叉子的控制和进餐的调用,它的格式如下:
output[i] = [a, b, c] (3个整数)
- a 哲学家编号。
- b 指定叉子:{1 : 左边, 2 : 右边}.
- c 指定行为:{1 : 拿起, 2 : 放下, 3 : 吃面}。
如 [4,2,1] 表示 4 号哲学家拿起了右边的叉子。

提示:

  • 1 <= n <= 60

解法

Java

ReentrantLock 和 Condition 的区别

  • ReentrantLock: 用于保护共享资源的互斥访问
  • Condition: 用于在已获取锁的线程之间进行协调和通信

为什么使用 ReentrantLock 而不是 Condition

  1. 资源模型匹配

    • 5根forks是独立的物理资源
    • 每个fork需要独立的互斥保护
    • ReentrantLock直接对应这种资源保护,而condition是某个资源共享的通信
  2. 问题本质

    • 哲学家就餐问题是典型的资源竞争问题
    • 关键在于如何避免死锁地获取多个资源
    • 不涉及复杂的线程间协调逻辑
  3. Condition 的适用场景

    • 通常用于生产者-消费者模式
    • 需要等待特定条件满足的场景
    • 例如:等待队列非空、等待缓冲区有空间等

代码示例说明

// 当前实现 - 直接使用 ReentrantLock 保护资源
private final Lock[] forks = new Lock[5];

// 如果使用 Condition,通常会是这样:
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();

在哲学家问题中,我们有5个独立的共享资源(叉子),所以使用5个ReentrantLock是最直接有效的方案。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class DiningPhilosophers {

    // 创建5个可重入锁,分别代表每根叉子
    private final Lock[] forks = new Lock[5];

    public DiningPhilosophers() {
        for (int i = 0; i < 5; i++) {
            forks[i] = new ReentrantLock();
        }
    }

    // philosopher: 哲学家编号 (0-4)
    // pickLeftFork: 拿起左叉
    // pickRightFork: 拿起右叉
    // eat: 进食
    // putLeftFork: 放下左叉
    // putRightFork: 放下右叉
    public void wantsToEat(int philosopher,
                           Runnable pickLeftFork,
                           Runnable pickRightFork,
                           Runnable eat,
                           Runnable putLeftFork,
                           Runnable putRightFork) throws InterruptedException {
        
        // 获取左右叉子的索引
        int leftFork = philosopher;
        int rightFork = (philosopher + 1) % 5;

        // 总是先获取编号较小的叉子,打破循环等待条件,预防死锁
        if (leftFork < rightFork) {
            forks[leftFork].lock();
            forks[rightFork].lock();
        } else {
            forks[rightFork].lock();
            forks[leftFork].lock();
        }

        try {
            // 执行操作
            pickLeftFork.run();
            pickRightFork.run();
            eat.run();
            putLeftFork.run();
            putRightFork.run();
        } finally {
            // 释放锁
            forks[leftFork].unlock();
            forks[rightFork].unlock();
        }
    }
}

Java

  • 直接获取的问题(会导致死锁)
  • 如果直接获取左右叉子,可能会出现以下情况:
  • 哲学家0: 先获取叉子0,再获取叉子1
  • 哲学家1: 先获取叉子1,再获取叉子2
  • 哲学家2: 先获取叉子2,再获取叉子3
  • 哲学家3: 先获取叉子3,再获取叉子4
  • 哲学家4: 先获取叉子4,再获取叉子0
  • 这时如果每个哲学家都拿到了第一根叉子,都在等待第二根叉子,就会形成循环等待,导致死锁。
  • 有序获取的好处
  • 通过总是先获取编号较小的叉子:
  • 哲学家0: 获取顺序 0→1
  • 哲学家4: 获取顺序 0→4(而不是4→0)
  • 这样就破坏了循环等待的条件,因为哲学家4会先等待叉子0,而不是先占用叉子4再等待叉子0,从而避免了死锁。

总结

这种策略属于资源有序分配法,是解决死锁问题的经典方法之一,通过打破"循环等待条件"来预防死锁的发生。

class DiningPhilosophers {
    
    // 使用Object数组表示5根叉子,每个叉子是一个锁对象
    private final Object[] forks = new Object[5];
    
    public DiningPhilosophers() {
        // 初始化5个锁对象,每个代表一根叉子
        for (int i = 0; i < 5; i++) {
            forks[i] = new Object();
        }
    }

    public void wantsToEat(int philosopher,
                           Runnable pickLeftFork,
                           Runnable pickRightFork,
                           Runnable eat,
                           Runnable putLeftFork,
                           Runnable putRightFork) throws InterruptedException {
        
        // 计算左右叉子的索引
        int leftFork = philosopher;
        int rightFork = (philosopher + 1) % 5;
        
        // 为了避免死锁,总是先获取编号较小的叉子
        Object firstFork;
        Object secondFork;
        if (leftFork < rightFork) {
            firstFork = forks[leftFork];
            secondFork = forks[rightFork];
        } else {
            firstFork = forks[rightFork];
            secondFork = forks[leftFork];
        }

        // 同步块确保原子性操作
        synchronized (firstFork) {
            synchronized (secondFork) {
                // 拿起叉子
                pickLeftFork.run();
                pickRightFork.run();
                
                // 进食
                eat.run();
                
                // 放下叉子
                putLeftFork.run();
                putRightFork.run();
            }
        }
    }
}