Java線程同步問題--哲學(xué)家就餐

1.場景
有五位沉默的哲學(xué)家圍坐在一張圓桌旁,他們一生都在吃東西和思考。
有五只筷子供他們使用,哲學(xué)家需要雙手拿到一雙筷子之后才能吃飯;吃完后會將筷子放下繼續(xù)思考。
那么現(xiàn)在有一個問題,我們需要想出一種方案,如何保證哲學(xué)家們可以交替吃飯和思考,而不會被餓死。

上面這個問題是由Dijkstra提出的一個經(jīng)典的線程同步問題。
2.解決方案
我們在開始想如何解決問題之前,可以先將這個場景通過代碼還原,在程序中進(jìn)行建模。
每一只筷子可以看做是一個資源數(shù)據(jù),都可以被它兩邊的哲學(xué)家嘗試去獲取,并且同一時間只能由其中一人持有,這可以通過我們JUC包中的信號量Semaphore來表示。
然后,每個哲學(xué)家可以看做是一個線程,每個線程中的run方法內(nèi)容都是先進(jìn)行思考,然后試圖獲取左右兩邊的筷子吃飯,吃完飯后繼續(xù)思考。
通過上面的分析,我們的代碼實(shí)現(xiàn)如下:
/**
?* @author 小黑說Java
?* @ClassName DiningPhilosophers
?* @Description 哲學(xué)家就餐問題
?* @date 2022/2/6
?**/
@Slf4j
public class DiningPhilosophers implements Runnable {
? ? private final int id;
? ? public DiningPhilosophers(int id) {
? ? ? ? this.id = id;
? ? }
? ? private static final Random random = new Random(System.currentTimeMillis());
? ? private static final Semaphore[] forks = new Semaphore[5];
? ? // 初始化信號量,每個信號量為1,代表1只筷子
? ? static {
? ? ? ? forks[0] = new Semaphore(1);
? ? ? ? forks[1] = new Semaphore(1);
? ? ? ? forks[2] = new Semaphore(1);
? ? ? ? forks[3] = new Semaphore(1);
? ? ? ? forks[4] = new Semaphore(1);
? ? }
? ? @Override
? ? public void run() {
? ? ? ? try {
? ? ? ? ? ? while (true) {
? ? ? ? ? ? ? ? think();
? ? ? ? ? ? ? ? eat(id);
? ? ? ? ? ? }
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? log.error("異常中斷", e);
? ? ? ? }
? ? }
? ? /**
? ? ?* 哲學(xué)家思考隨機(jī)時間
? ? ?*/
? ? private void think() throws InterruptedException {
? ? ? ? TimeUnit.MILLISECONDS.sleep(random.nextInt(100));
? ? }
? ? private void eat(int id) {
? ? ? ? // TODO
? ? }
}接下來,我們思考一下,如何實(shí)現(xiàn)哲學(xué)家吃飯的邏輯。
當(dāng)一個哲學(xué)家需要吃飯時,他要拿起左右兩邊的筷子。
所以:
- 哲學(xué)家 A(0) 需要筷子0 和 4
- 哲學(xué)家 B(1) 需要筷子 1 和 0
- 哲學(xué)家 C(2) 需要筷子 2 和 1
- 哲學(xué)家 D(3) 需要筷子 3 和 2
- 哲學(xué)家 E(4) 需要筷子 4 和 3
所以每個哲學(xué)家線程都應(yīng)該有個編號,所以我在DiningPhilosophers中定義了屬性id表示哲學(xué)家的編號。
在吃飯方法中,需要根據(jù)id來決定獲取哪只筷子。
左手邊的筷子可以有用fork[id]表示;
右手邊的筷子用fork[(id+4)%5]表示。
那么我們的eat方法的實(shí)現(xiàn)如下:
private void eat(int id) throws InterruptedException {
? ? // 先拿左邊的筷子
? ? forks[id].acquire();
? ? // 然后拿右邊的筷子
? ? forks[(id + 4) % 5].acquire();
? ? // 吃一口飯
? ? log.info("哲學(xué)家{}正在吃飯~", id);
? ? // 依次放下左邊的筷子和右邊的筷子
? ? forks[id].release();
? ? forks[(id + 4) % 5].release();
}我們接著來測試我們的完整代碼。
/**
?* @author 小黑說Java
?* @ClassName DiningPhilosophers
?* @Description 哲學(xué)家就餐問題
?* @date 2022/2/6
?**/
@Slf4j
public class DiningPhilosophers implements Runnable {
? ? private final int id;
? ? public DiningPhilosophers(int id) {
? ? ? ? this.id = id;
? ? }
? ? private static final Random random = new Random(System.currentTimeMillis());
? ? private static final Semaphore[] forks = new Semaphore[5];
? ? // 初始化信號量,每個信號量為1,代表1只筷子
? ? static {
? ? ? ? forks[0] = new Semaphore(1);
? ? ? ? forks[1] = new Semaphore(1);
? ? ? ? forks[2] = new Semaphore(1);
? ? ? ? forks[3] = new Semaphore(1);
? ? ? ? forks[4] = new Semaphore(1);
? ? }
? ? @Override
? ? public void run() {
? ? ? ? try {
? ? ? ? ? ? while (true) {
? ? ? ? ? ? ? ? think();
? ? ? ? ? ? ? ? eat(id);
? ? ? ? ? ? }
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? log.error("異常中斷", e);
? ? ? ? }
? ? }
? ? /**
? ? ?* 哲學(xué)家思考隨機(jī)時間
? ? ?*/
? ? private void think() throws InterruptedException {
? ? ? ? TimeUnit.MILLISECONDS.sleep(random.nextInt(100));
? ? }
? ? private void eat(int id) throws InterruptedException {
? ? ? ? // 先拿左邊的筷子
? ? ? ? forks[id].acquire();
? ? ? ? // 然后拿右邊的筷子
? ? ? ? forks[(id + 4) % 5].acquire();
? ? ? ? // 吃一口飯
? ? ? ? log.info("哲學(xué)家{}正在吃飯~", id);
? ? ? ? // 依次放下左邊的筷子和右邊的筷子
? ? ? ? forks[id].release();
? ? ? ? forks[(id + 4) % 5].release();
? ? }
? ? public static void main(String[] args) {
? ? ? ? for (int i = 0; i < 5; i++) {
? ? ? ? ? ? new Thread(new DiningPhilosophers(i)).start();
? ? ? ? }
? ? }
}運(yùn)行上面的代碼后,會發(fā)現(xiàn)程序在運(yùn)行一段時間后會進(jìn)入死鎖狀態(tài)。
這種情況是因?yàn)?,在某一時刻,所有的哲學(xué)家都獲取到了左手邊的筷子,而無法獲取到右手邊的筷子,導(dǎo)致沒有人可以到東西,陷入僵局。
該如何避免出現(xiàn)這種死鎖問題呢?
方法一:限制吃飯的哲學(xué)家人數(shù)
很簡單的一種方法,就是在一個時間點(diǎn),只能有最多4個哲學(xué)家開始吃飯。4個哲學(xué)家分5只筷子,則永遠(yuǎn)不會發(fā)生死鎖。
要實(shí)現(xiàn)這種方法,我們可以再定義一個許可數(shù)為4的信號量Semaphore,表示剩余可以吃飯的哲學(xué)家名額。
代碼實(shí)現(xiàn)如下:
/**
?* @author 小黑說Java
?* @ClassName DiningPhilosophers
?* @Description 哲學(xué)家就餐問題
?* @date 2022/2/6
?**/
@Slf4j
public class DiningPhilosophers implements Runnable {
? ? private final int id;
? ? public DiningPhilosophers(int id) {
? ? ? ? this.id = id;
? ? }
? ? private static final Random random = new Random(System.currentTimeMillis());
? ? private static final Semaphore[] forks = new Semaphore[5];
? ? private static final Semaphore maxDiners = new Semaphore(4);
? ? // 初始化信號量,每個信號量為1,代表1只筷子
? ? static {
? ? ? ? forks[0] = new Semaphore(1);
? ? ? ? forks[1] = new Semaphore(1);
? ? ? ? forks[2] = new Semaphore(1);
? ? ? ? forks[3] = new Semaphore(1);
? ? ? ? forks[4] = new Semaphore(1);
? ? }
? ? @Override
? ? public void run() {
? ? ? ? try {
? ? ? ? ? ? while (true) {
? ? ? ? ? ? ? ? think();
? ? ? ? ? ? ? ? eat(id);
? ? ? ? ? ? }
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? log.error("異常中斷", e);
? ? ? ? }
? ? }
? ? /**
? ? ?* 哲學(xué)家思考隨機(jī)時間
? ? ?*/
? ? private void think() throws InterruptedException {
? ? ? ? TimeUnit.MILLISECONDS.sleep(random.nextInt(100));
? ? }
? ? private void eat(int id) throws InterruptedException {
? ? ? ? // 先獲得吃飯名額
? ? ? ? maxDiners.acquire();
? ? ? ? // 先拿左邊的筷子
? ? ? ? forks[id].acquire();
? ? ? ? // 然后拿右邊的筷子
? ? ? ? forks[(id + 4) % 5].acquire();
? ? ? ? // 吃一口飯
? ? ? ? log.info("哲學(xué)家{}正在吃飯~", id);
? ? ? ? // 依次放下左邊的筷子和右邊的筷子
? ? ? ? forks[id].release();
? ? ? ? forks[(id + 4) % 5].release();
? ? ? ? // 吃完之后歸還吃飯名額
? ? ? ? maxDiners.release();
? ? }
? ? public static void main(String[] args) {
? ? ? ? for (int i = 0; i < 5; i++) {
? ? ? ? ? ? new Thread(new DiningPhilosophers(i)).start();
? ? ? ? }
? ? }
}方法二:找到一個左撇子哲學(xué)家
這種方法是讓其中一個哲學(xué)家和其他哲學(xué)家拿筷子的順序和其他哲學(xué)家不一樣。
比如其他人都是先拿右手邊再拿左手邊,而這個左撇子哲學(xué)家則先拿左手邊再拿右手邊。
而哪一位哲學(xué)家被選為左撇子并不重要,因?yàn)樽雷邮菆A的,所以我們就選擇0號哲學(xué)家為左撇子。
代碼實(shí)現(xiàn)如下:
/**
?* @ClassName DiningPhilosophers
?* @Description 哲學(xué)家就餐問題
?* @date 2022/2/6
?**/
@Slf4j
public class DiningPhilosophers implements Runnable {
? ? private final int id;
? ? public DiningPhilosophers(int id) {
? ? ? ? this.id = id;
? ? }
? ? private static final Random random = new Random(System.currentTimeMillis());
? ? private static final Semaphore[] forks = new Semaphore[5];
? ? // 初始化信號量,每個信號量為1,代表1只筷子
? ? static {
? ? ? ? forks[0] = new Semaphore(1);
? ? ? ? forks[1] = new Semaphore(1);
? ? ? ? forks[2] = new Semaphore(1);
? ? ? ? forks[3] = new Semaphore(1);
? ? ? ? forks[4] = new Semaphore(1);
? ? }
? ? @Override
? ? public void run() {
? ? ? ? try {
? ? ? ? ? ? while (true) {
? ? ? ? ? ? ? ? think();
? ? ? ? ? ? ? ? eat(id);
? ? ? ? ? ? }
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? log.error("異常中斷", e);
? ? ? ? }
? ? }
? ? /**
? ? ?* 哲學(xué)家思考隨機(jī)時間
? ? ?*/
? ? private void think() throws InterruptedException {
? ? ? ? TimeUnit.MILLISECONDS.sleep(random.nextInt(100));
? ? }
? ? private void eat(int id) throws InterruptedException {
? ? ? ? if (id == 0) {
? ? ? ? ? ? hanleLeftFirst(id);
? ? ? ? } else {
? ? ? ? ? ? hanldRightFirst(id);
? ? ? ? }
? ? ? ? // 吃一口飯
? ? ? ? log.info("哲學(xué)家{}正在吃飯~", id);
? ? ? ? forks[id].release();
? ? ? ? forks[(id + 4) % 5].release();
? ? }
? ? private void hanleLeftFirst(int id) throws InterruptedException {
? ? ? ? forks[id].acquire();
? ? ? ? forks[(id + 4) % 5].acquire();
? ? }
? ? private void hanldRightFirst(int id) throws InterruptedException {
? ? ? ? forks[(id + 4) % 5].acquire();
? ? ? ? forks[id].acquire();
? ? }
? ? public static void main(String[] args) {
? ? ? ? for (int i = 0; i < 5; i++) {
? ? ? ? ? ? new Thread(new DiningPhilosophers(i)).start();
? ? ? ? }
? ? }
}到此這篇關(guān)于Java線程同步問題的文章就介紹到這了,更多相關(guān)Java線程同步內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java處理InterruptedException異常的理論與實(shí)踐
在使用Java的過程中,有個情景或許很多人見過,您在編寫一個測試程序,程序需要暫停一段時間,于是調(diào)用 Thread.sleep()。但是編譯器或 IDE 報(bào)錯說沒有處理檢查到的 InterruptedException。InterruptedException 是什么呢,為什么必須處理它?下面跟著小編一起來看看。2016-08-08
java使用EasyExcel實(shí)現(xiàn)Sheet的復(fù)制與填充
EasyExcel是一個非常有用的工具,它提供了強(qiáng)大的模板填充功能,可以輕松解決各種業(yè)務(wù)需求,本文主要為大家介紹了如何使用EasyExcel實(shí)現(xiàn)模板Sheet復(fù)制與填充,需要的可以參考下2023-10-10
Java中替代equals,compareTo和toString的方法
這篇文章主要介紹了Java中替代equals,compareTo和toString的方法,文中代碼十分詳細(xì),幫助大家更好的理解的學(xué)習(xí),感興趣的朋友可以了解下2020-06-06
Java Web制作登錄驗(yàn)證碼實(shí)現(xiàn)代碼解析
這篇文章主要介紹了Java Web制作登錄驗(yàn)證碼實(shí)現(xiàn)代碼解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-09-09
java unicode轉(zhuǎn)碼為中文實(shí)例
這篇文章主要介紹了java unicode轉(zhuǎn)碼為中文的實(shí)例,大家參考使用吧2013-12-12

