• LeetCode——哲学家进餐问题


    Q:5 个沉默寡言的哲学家围坐在圆桌前,每人面前一盘意面。叉子放在哲学家之间的桌面上。(5 个哲学家,5 根叉子)
    所有的哲学家都只会在思考和进餐两种行为间交替。哲学家只有同时拿到左边和右边的叉子才能吃到面,而同一根叉子在同一时间只能被一个哲学家使用。每个哲学家吃完面后都需要把叉子放回桌面以供其他哲学家吃面。只要条件允许,哲学家可以拿起左边或者右边的叉子,但在没有同时拿到左右叉子时不能进食。
    假设面的数量没有限制,哲学家也能随便吃,不需要考虑吃不吃得下。
    设计一个进餐规则(并行算法)使得每个哲学家都不会挨饿;也就是说,在没有人知道别人什么时候想吃东西或思考的情况下,每个哲学家都可以在吃饭和思考之间一直交替下去。

    哲学家从 0 到 4 按 顺时针 编号。请实现函数 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 号哲学家拿起了右边的叉子。

    A:
    引用:@̶.̶G̶F̶u̶'̶ 、̶ ̶|
    这个题目是防止死锁,每个哲学家都拿起左手或右手,导致死锁
    1.第一种方法是设置一个信号量,当前哲学家会同时拿起左手和右手的叉子直至吃完。即有3 个人中,2 个人各自持有 2 个叉子,1 个人持有 1 个叉子,共计 5 个叉子。
    用Semaphore去实现上述的限制:Semaphore eatLimit = new Semaphore(4);
    一共有5个叉子,视为5个ReentrantLock,并将它们全放入1个数组中。
    设置编码:

    代码:

    class DiningPhilosophers {
        //1个Fork视为1个ReentrantLock,5个叉子即5个ReentrantLock,将其都放入数组中
        private ReentrantLock[] locks = {new ReentrantLock(), new ReentrantLock(), new ReentrantLock(), new ReentrantLock(), new ReentrantLock()};
        //限制 最多只有4个哲学家去持有叉子
        private Semaphore eatLimit = new Semaphore(4);
    
        public DiningPhilosophers() {
    
        }
    
        // call the run() method of any runnable to execute its code
        public void wantsToEat(int philosopher,
                               Runnable pickLeftFork,
                               Runnable pickRightFork,
                               Runnable eat,
                               Runnable putLeftFork,
                               Runnable putRightFork) throws InterruptedException {
            int leftFork = (philosopher + 1) % 5;//左边的叉子 的编号
            int rightFork = philosopher;//右边的叉子 的编号
    
            eatLimit.acquire();//限制人数减一
    
            locks[leftFork].lock();
            locks[rightFork].lock();
    
            pickLeftFork.run();
            pickRightFork.run();
    
            eat.run();
    
            putLeftFork.run();
            putRightFork.run();
    
            locks[leftFork].unlock();
            locks[rightFork].unlock();
    
            eatLimit.release();
        }
    }
    

    2.设置 1 个临界区以实现 1 个哲学家 “同时”拿起左右 2 把叉子的效果。即进入临界区之后,保证成功获取到左右 2 把叉子 并 执行相关代码后,才退出临界区。
    与上一种的差别是“允许1个哲学家用餐”。方法2是在成功拿起左右叉子之后就退出临界区,而“只让1个哲学家就餐”是在拿起左右叉子 + 吃意面 + 放下左右叉子 一套流程走完之后才退出临界区。
    前者的情况可大概分为2种,举具体例子说明(可参照上面给出的图片):

    • 1号哲学家拿起左右叉子(1号叉子 + 2号叉子)后就退出临界区,此时4号哲学家成功挤进临界区,他也成功拿起了左右叉子(0号叉子和4号叉子),然后就退出临界区。
    • 1号哲学家拿起左右叉子(1号叉子 + 2号叉子)后就退出临界区,此时2号哲学家成功挤进临界区,他需要拿起2号叉子和3号叉子,但2号叉子有一定的概率还被1号哲学家持有(1号哲学家意面还没吃完),因此2号哲学家进入临界区后还需要等待2号叉子。至于3号叉子,根本没其他人跟2号哲学家争夺,因此可以将该种情况视为“2号哲学家只拿起了1只叉子,在等待另1只叉子”的情况。

    总之,第1种情况即先后进入临界区的2位哲学家的左右叉子不存在竞争情况,因此先后进入临界区的2位哲学家进入临界区后都不用等待叉子,直接就餐。此时可视为2个哲学家在同时就餐(当然前1个哲学家有可能已经吃完了,但姑且当作是2个人同时就餐)。
    第2种情况即先后进入临界区的2位哲学家的左右叉子存在竞争情况(说明这2位哲学家的编号相邻),因此后进入临界区的哲学家还需要等待1只叉子,才能就餐。此时可视为只有1个哲学家在就餐。
    至于“只允许1个哲学家就餐”的代码,很好理解,每次严格地只让1个哲学家就餐,由于过于严格,以至于都不需要将叉子视为ReentrantLock。
    方法2有一定的概率是“并行”,“只允许1个哲学家就餐”是严格的“串行”。

    代码:

    class DiningPhilosophers {
        //1个Fork视为1个ReentrantLock,5个叉子即5个ReentrantLock,将其都放入数组中
    	private ReentrantLock[] lockList = {new ReentrantLock(),
    		new ReentrantLock(),
    		new ReentrantLock(),
    		new ReentrantLock(),
    		new ReentrantLock()};
        
        //让 1个哲学家可以 “同时”拿起2个叉子(搞个临界区)
    	private ReentrantLock pickBothForks = new ReentrantLock();
    
    	public DiningPhilosophers() {
    
    	}
    
    	// call the run() method of any runnable to execute its code
    	public void wantsToEat(int philosopher,
    		Runnable pickLeftFork,
    		Runnable pickRightFork,
    		Runnable eat,
    		Runnable putLeftFork,
    		Runnable putRightFork) throws InterruptedException {
            
    		int leftFork = (philosopher + 1) % 5;	//左边的叉子 的编号
    		int rightFork = philosopher;	//右边的叉子 的编号
    
    		pickBothForks.lock();	//进入临界区
    
    		lockList[leftFork].lock();	//拿起左边的叉子
    		lockList[rightFork].lock();	//拿起右边的叉子
    
    		pickLeftFork.run();	//拿起左边的叉子 的具体执行
    		pickRightFork.run();	//拿起右边的叉子 的具体执行
            
    		pickBothForks.unlock();	//退出临界区
    
    		eat.run();	//吃意大利面 的具体执行
    
    		putLeftFork.run();	//放下左边的叉子 的具体执行
    		putRightFork.run();	//放下右边的叉子 的具体执行
    
    		lockList[leftFork].unlock();	//放下左边的叉子
    		lockList[rightFork].unlock();	//放下右边的叉子
    	}
    }
    

    3.前面说过,该题的本质是考察 如何避免死锁。
    而当5个哲学家都左手持有其左边的叉子 或 当5个哲学家都右手持有其右边的叉子时,会发生死锁。
    故只需设计1个避免发生上述情况发生的策略即可。
    即可以让一部分哲学家优先去获取其左边的叉子,再去获取其右边的叉子;再让剩余哲学家优先去获取其右边的叉子,再去获取其左边的叉子。
    代码:

    class DiningPhilosophers {
    	//1个Fork视为1个ReentrantLock,5个叉子即5个ReentrantLock,将其都放入数组中
    	private ReentrantLock[] lockList = {new ReentrantLock(),
    		new ReentrantLock(),
    		new ReentrantLock(),
    		new ReentrantLock(),
    		new ReentrantLock()};
    
    	public DiningPhilosophers() {
    
    	}
    
    	// call the run() method of any runnable to execute its code
    	public void wantsToEat(int philosopher,
    		Runnable pickLeftFork,
    		Runnable pickRightFork,
    		Runnable eat,
    		Runnable putLeftFork,
    		Runnable putRightFork) throws InterruptedException {
    
    		int leftFork = (philosopher + 1) % 5;    //左边的叉子 的编号
    		int rightFork = philosopher;    //右边的叉子 的编号
    
            //编号为偶数的哲学家,优先拿起左边的叉子,再拿起右边的叉子
    		if (philosopher % 2 == 0) {
    			lockList[leftFork].lock();    //拿起左边的叉子
    			lockList[rightFork].lock();    //拿起右边的叉子
    		}
            //编号为奇数的哲学家,优先拿起右边的叉子,再拿起左边的叉子
    		else {
    			lockList[rightFork].lock();    //拿起右边的叉子
    			lockList[leftFork].lock();    //拿起左边的叉子
    		}
    
    		pickLeftFork.run();    //拿起左边的叉子 的具体执行
    		pickRightFork.run();    //拿起右边的叉子 的具体执行
    
    		eat.run();    //吃意大利面 的具体执行
    
    		putLeftFork.run();    //放下左边的叉子 的具体执行
    		putRightFork.run();    //放下右边的叉子 的具体执行
    
    		lockList[leftFork].unlock();    //放下左边的叉子
    		lockList[rightFork].unlock();    //放下右边的叉子
    	}
    }
    

    改进:改进代码看3种解法(互斥锁或volatile)
    1.ReentrantLock和synchronize关键字都是使用互斥量的重量级锁,而volatile关键字相较于它们就比较“轻量”。
    因此把ReentrantLock数组改为使用volatile修饰的boolean数组。
    PS: volatile要和原子操作搭配使用才能保证同步。
    而对volatile变量赋 常量值 可看为是原子操作。

    看着后面这种解法更清晰:
    每个人都可以尝试去吃东西,吃东西前尝试去拿左边的叉子和右边的叉子,这样就可以想到使用信号量Semaphore的tryAcquire方法。
    这里竞争的资源是叉子,所以定义代表5个叉子的信号量即可。
    代码:

    class DiningPhilosophers {
        int num = 5;
        //五个叉子的信号量
        private Semaphore[] semaphores = new Semaphore[5];
    
        public DiningPhilosophers() {
            for (int i = 0; i < num; i++) {
                //每只叉子只有1个
                semaphores[i] = new Semaphore(1);
            }
    
        }
    
        // call the run() method of any runnable to execute its code
        public void wantsToEat(int philosopher,
                               Runnable pickLeftFork,
                               Runnable pickRightFork,
                               Runnable eat,
                               Runnable putLeftFork,
                               Runnable putRightFork) throws InterruptedException {
            //左边叉子的位置
            int left = philosopher;
            //右边叉子的位置
            int right = (philosopher + 1) % num;
            while (true) {
                if (semaphores[left].tryAcquire()) {
                    //先尝试获取左边叉子,如果成功再尝试获取右边叉子
                    if (semaphores[right].tryAcquire()) {
                        //两个叉子都得到了,进餐
                        pickLeftFork.run();
                        pickRightFork.run();
                        eat.run();
                        putLeftFork.run();
                        //释放左边叉子
                        semaphores[left].release();
                        putRightFork.run();
                        //释放右边边叉子
                        semaphores[right].release();
    
                        //吃完了,就跳出循环
                        break;
                    } else {
                        //如果拿到了左边的叉子,但没拿到右边的叉子: 就释放左边叉子
                        semaphores[left].release();
                        //让出cpu等一会
                        Thread.yield();
                    }
                } else {
                    //连左边叉子都没拿到,就让出cpu等会吧
                    Thread.yield();
                }
            }
    
        }
    
    }
    
  • 相关阅读:
    java 与打卡器通过udp协议交互
    java串口通信与打卡器交互
    hibernate 学习小结
    Log4J使用说明
    【秋招必备】Git常用命令(2021最新版)
    【秋招必备】Java集合面试题(2021最新版)
    工作这么多年!很多人竟然不知道线程池的创建方式有7种?
    【秋招必备】Java虚拟机面试题(2021最新版)
    【秋招必备】java异常面试题(2021最新版)
    好未来面试官:说说强引用、软引用、弱引用、幻象引用有什么区别?
  • 原文地址:https://www.cnblogs.com/xym4869/p/12742950.html
Copyright © 2020-2023  润新知