4.哲学家就餐问题

314 阅读4分钟

哲学家就餐问题

有 5 个哲学家,他们面前都有一双筷子,即左手有一根筷子,右手有一根筷子。当然,这个问题有多个版本的描述,可以说是筷子,也可以说是一刀一叉,因为吃牛排的时候,需要刀和叉,缺一不可,也有说是用两把叉子来吃意大利面。这里具体是刀叉还是筷子并不重要,重要的是必须要同时持有左右两边的两个才行,也就是说,哲学家左手要拿到一根筷子,右手也要拿到一根筷子,在这种情况下哲学家才能吃饭。为了方便理解,我们选取和我国传统最贴近的筷子来说明这个问题。 为什么选择哲学家呢?因为哲学家的特点是喜欢思考,所以我们可以把哲学家一天的行为抽象为思考,然后吃饭,并且他们吃饭的时候要用一双筷子,而不能只用一根筷子。

主流程

我们来看一下哲学家就餐的主流程。哲学家如果想吃饭,他会先尝试拿起左手的筷子,然后再尝试拿起右手的筷子,如果某一根筷子被别人使用了,他就得等待他人用完,用完之后他人自然会把筷子放回原位,接着他把筷子拿起来就可以吃了(不考虑卫生问题)。这就是哲学家就餐的最主要流程。

while(true){
    think();
    pick_up_left_chopstick();
    pick_up_right_chopstick();
    eat();
    pick_down_right_chopstick();
    pick_down_left_chopstick();
}

死锁和资源耗尽问题

每个人都拿着左手的筷子,都缺少右手的筷子,那么就没有人可以开始吃饭了,自然也就没有人会放下手中的筷子。这就陷入了死锁,形成了一个相互等待的情况。

package com.test;
​
public class DiningPhilosophers {
     public static class Philosopher implements Runnable {
        private Object leftChopstick;
        private Object rightChopstick;
​
        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }
​
        @Override
        public void run() {
            try {
                while (true) {
                    doAction("思考人生、宇宙、万物、灵魂...");
                    synchronized (leftChopstick) {
                        doAction("拿起左边的筷子");
                        synchronized (rightChopstick) {
                            doAction("拿起右边的筷子");
                            doAction("吃饭");
                            doAction("放下右边的筷子");
                        }
                        doAction("放下左边的筷子");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
​
        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) (Math.random() * 10));
        }
    }
​
    public static void main(String[] args) {
        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i];
            Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
            philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
            new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
        }
    }
}

解决方案

  1. 服务员检查

    1. 比如我们引入一个服务员,当每次哲学家要吃饭时,他需要先询问服务员:我现在能否去拿筷子吃饭?此时,服务员先判断他拿筷子有没有发生死锁的可能,假如有的话,服务员会说:现在不允许你吃饭。这是一种解决方案。
  2. 领导调节

    1. 可以引入一个领导,这个领导进行定期巡视。如果他发现已经发生死锁了,就会剥夺某一个哲学家的筷子,让他放下。这样一来,由于这个人的牺牲,其他的哲学家就都可以吃饭了。这也是一种解决方案。
  3. 改变一个哲学家拿筷子的顺序

    1. 我们实例化哲学家对象的时候,传入的参数原本都是先传入左边的筷子再传入右边的,但是当我们发现他是最后一个哲学家的时候,也就是 if (i == philosophers.length - 1) ,在这种情况下,我们给它传入的筷子顺序恰好相反,这样一来,他拿筷子的顺序也就相反了,他会先拿起右边的筷子,再拿起左边的筷子。那么这个程序运行的结果,是所有哲学家都可以正常地去进行思考和就餐了,并且不会发生死锁。
public static void main(String[] args) {
    Philosopher[] philosophers = new Philosopher[5];
    Object[] chopsticks = new Object[philosophers.length];
    for (int i = 0; i < chopsticks.length; i++) {
        chopsticks[i] = new Object();
    }
    for (int i = 0; i < philosophers.length; i++) {
        Object leftChopstick = chopsticks[i];
        Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
        if (i == philosophers.length - 1) {
            philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
        } else {
            philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
        }
        new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
    }
}

\