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

    }

}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM