操作系統實驗之進程管理——生產者消費者問題

實驗要求

  • 選擇一個經典的同步問題(生產者-消費者問題、讀者-寫者問題、哲學家就餐問題等),並模擬實現該同步問題的進程管理;
  • 採用信號量機制及相應的P操作、V操作;
  • 應避免出現死鎖;
  • 能夠顯示相關的狀態。

我這裏選擇的是生產者消費者問題,使用java實現
源碼上傳到了本人github

實驗原理

代碼仿照某個博主的思想重寫的,本來想貼出來博主地址,但是忘了是哪位博主,如果日後找到了地址會再貼出來,實在抱歉。

代碼結構

  • 消費者Consumer作爲消費者類,私有屬性有id、消費數、倉庫,方法有消費,消費實際是調用構造時傳入的倉庫中的消費方法。
  • 生產者Producer作爲生產者類,私有屬性有id、生產數、倉庫,方法有生產,生產實際是調用構造時傳入的倉庫中的生產方法。
  • 倉庫Storage作爲倉庫類,私有屬性有一個LinkedList作爲商品存放的格子,Max_size是格子數。Storage封裝了消費者/生產者對倉庫取產品/存產品的方法。在這裏,LinkedList是本例中的公共區。
  • 主類中實例化了倉庫,並用在實例化的時候將該倉庫傳給了消費者生產者,分別給消費者生產者設置了消費數量

核心代碼

倉庫中消費方法

    public void consume(int num, int id) {
        synchronized (list) {
            while (list.size() < num) {
                System.out.println("【消費者" + id + "】預計取出產品數量:" + num + "\t當前庫存:"
                        + list.size() + "\t產品太少,不夠消費。等待中……");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.print("【消費者" + id + "】預計取出產品數量:" + num + "\n"
                    + "【消費者" + id + "】正在取出中……");
            //鎖住print輸出,防止被其他線程的輸出打斷
            synchronized (System.out) {
                for (int i = 1; i <= num; ++i) {
                    try {
                        sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    list.remove();
                    System.out.print(i + "...");
                }
                System.out.println();
            }
            list.notifyAll();
        }
        System.out.println("【消費者" + id + "】已取出:" + num + "\t當前庫存:" + list.size());
        System.out.println("【消費者" + id + "】正在消費產品……");
        try {
            sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("【消費者" + id + "】消費結束");
    }

倉庫中生產方法

    public void produce(int num, int id) {
        System.out.println("【生產者" + id + "】正在生產產品……");
        try {
            sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("【生產者" + id + "】生產結束");
        synchronized (list) {
            while (list.size() + num > MAX_SIZE) {
                System.out.println("【生產者" + id + "】已生產產品:" + num + "\t當前庫存:"
                        + list.size() + "\t倉庫已滿,不能存入");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.print("【生產者" + id + "】預計存入倉庫數量:" + num + "\n"
                    + "【生產者" + id + "】正在存入中……");
            //鎖住print輸出,防止被其他線程的輸出打斷
            synchronized (System.out) {
                for (int i = 1; i <= num; ++i) {
                    try {
                        sleep(400);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    list.add(new Object());
                    System.out.print(i + "...");
                }
                System.out.println();
            }

            System.out.println("【生產者" + id + "】已存入:" + num + "\t當前庫存:" + list.size());

            list.notifyAll();
        }
    }

代碼思路

其實思路並不複雜,仔細看同步區的代碼就會發現,它們基本滿足下面代碼的格式。

synchronized(){
    while(條件不滿足){
        wait();//將線程掛起
    }
    執行某操作;//比如生產結束後將產品放入倉庫
    notifyAll();//執行結束後將掛起的線程喚醒
}

synchronized(list)包裹起來的代碼塊是同步代碼段,含義簡單來說即當有線程訪問這一部分代碼時(實際測試是訪問list對象時),會給其加鎖,那麼其他線程訪問list時會被阻塞,等待該線程結束list對象的訪問後才能繼續訪問。
而這裏while(條件不滿足)則是具體問題的條件,比如消費者想消費30個產品,首先倉庫裏得有30個產品才行吧?所以需要這裏有代碼讓不滿足條件的線程掛起。
對應到操作系統裏講的pv原語,其實java關於pv原語的操作就是synchronized(list)和while字段,我們對應着來看:首先,在操作系統裏關於list有一個信號量是是否有人在操作倉庫,這個信號量初始值是1,那麼synchronized(list)的左大括號和右大括號分別對應這個信號量的p操作和v操作。其次,操作系統中關於倉庫的空間大小有限度,那麼倉庫當前大小這個信號量是0,倉庫餘空間是100,這兩個信號量的pv操作就對應了while部分。在操作系統中,作爲生產者,開始拿出產品時需要首先對當前倉庫剩餘空間這個信號量進行p操作,放一個商品就p操作一次,放完了一個商品就要對倉庫當前大小這個信號量進行v操作,100就逐漸減小,0逐漸增大。這裏的p和v分別對應while部分,當100減到0的時候,沒有空間放商品了,while條件不再滿足,那麼該線程需要被掛起,生產者就得等待消費者先來消費。消費者是同理,這裏不予贅述。

另外我代碼裏有些輸出也被synchronized包裹起來了,這裏解釋一下是因爲print()方法是會被異步執行的,也就是說我本來預定某個生產者/消費者隔0.3s輸出1…2…3…這樣模擬存入/取出的過程,如果在輸出的時候正好有某個消費者/生產者它生產/消費結束了,那麼它也要輸出一行話叫“生產結束”/“消費結束”,如果不包裹起來,輸出會被打斷,出現這樣尷尬的情況

【生產者2】正在存入中……1…2…3…4…5…6…7…8…【生產者1】生產結束
9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…
【生產者2】已存入:30 當前庫存:30

這裏就是生產者1發出其生產結束準備存入產品的消息打斷了正在存入產品的生產者2的輸出,將這個輸出過程加上synchronized(System.out)幷包裹起來即可解決。(但是這樣產生的問題是生產者1生產完成後不能能及時的打印出自己生產完成了的訊息。嘛畢竟打印機只有一臺,只能這麼解決了)

代碼結果

【生產者1】正在生產產品……
【生產者2】正在生產產品……
【生產者3】正在生產產品……
【生產者4】正在生產產品……
【消費者1】預計取出產品數量:50 當前庫存:0 產品太少,不夠消費。等待中……
【消費者2】預計取出產品數量:20 當前庫存:0 產品太少,不夠消費。等待中……
【消費者3】預計取出產品數量:30 當前庫存:0 產品太少,不夠消費。等待中……
【生產者2】生產結束
【生產者2】預計存入倉庫數量:30
【生產者2】正在存入中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…
【生產者2】已存入:30 當前庫存:30
【生產者1】生產結束
【消費者3】預計取出產品數量:30
【消費者3】正在取出中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…
【消費者3】已取出:30 當前庫存:0
【生產者3】生產結束
【生產者4】生產結束
【消費者2】預計取出產品數量:20 當前庫存:0 產品太少,不夠消費。等待中……
【消費者3】正在消費產品……
【消費者1】預計取出產品數量:50 當前庫存:0 產品太少,不夠消費。等待中……
【生產者4】預計存入倉庫數量:30
【生產者4】正在存入中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…
【消費者3】消費結束
【生產者4】已存入:30 當前庫存:30
【生產者3】預計存入倉庫數量:30
【生產者3】正在存入中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…
【生產者3】已存入:30 當前庫存:60
【生產者1】預計存入倉庫數量:30
【生產者1】正在存入中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…
【生產者1】已存入:30 當前庫存:90
【消費者1】預計取出產品數量:50
【消費者1】正在取出中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…31…32…33…34…35…36…37…38…39…40…41…42…43…44…45…46…47…48…49…50…
【消費者1】已取出:50 當前庫存:40
【消費者1】正在消費產品……
【消費者2】預計取出產品數量:20
【消費者2】正在取出中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…
【消費者2】已取出:20 當前庫存:20
【消費者2】正在消費產品……
【消費者1】消費結束
【消費者2】消費結束

Process finished with exit code 0

馬後炮

調試過程

這一部分記錄我自己在寫代碼時踩到的坑。

關於synchronized到底同步的是啥,經過多方詢問以及自己測試,最終發現確實有些是synchronized包裹的代碼段也會被異步執行,所以synchronized同步的是括號中的對象,對於不是這個對象的操作是會被異步執行的。假設我有句輸出在這個代碼段裏,那麼它是可以被直接訪問到的,而不會被加鎖。也就是synchronized鎖是鎖在了括號裏的對象上。其實這一點也很好理解,一個方面來說這正好是pv原語的體現,pv原語只對信號量操作,而不關注代碼本身,而synchronized本身鎖住的是某個對象,而不是代碼也正好能體現pv操作。另一個方面,在多線程處理的時候,本身就應該儘可能的減小鎖的粒度,不是同步所需要的萬不得已之時,儘可能的少去使用鎖,這樣才能加大效率。

存在的問題

其實這個代碼的封裝程度不算高,我有意將對倉庫的存入和拿出操作寫在裏倉庫裏,不過問題在於沒有將存入和取出這兩個對倉庫的動作以及生產和消費這兩個消費者生產者本身的操作分開。而是通通放在了倉庫的消費、生產操作裏。其實我寫到了後面才發現,生產商品和把生產的東西放進倉庫應該要分開來寫,在代碼結果裏可以看到我已經做了劃分,然而更好的解決方案應該是把生產的代碼整個封裝到生產者類裏,把消費的代碼整個封裝到消費者類裏。這樣倉庫類就是單純的做存入和取出操作,降低代碼耦合度。

小結

在進程間通訊、synchronized方面查閱了許多資料才最終理解了java多線程的具體實現原理,最後對整個概念都有了進一步的認識。最有意思的是自己試着將操作系統的pv操作和java多線程的同步進行了比較和對應,其實可以認爲java中synchronized等對pv原語的封裝實際也是爲了簡化pv操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章