併發王者課-青銅10:千錘百煉-如何解決生產者與消費者經典問題

秦二爺發表於2021-06-10

歡迎來到《併發王者課》,本文是該系列文章中的第10篇

在本篇文章中,我將為你介紹併發中的經典問題-生產者與消費者問題,並基於前面系列文章的知識點,通過wait、notify實現這一問題的簡版方案

一、生產者與消費者問題

生產者消費者問題(Producer-consumer problem),也稱有限緩衝問題(Bounded-buffer problem),是一個多程式、執行緒同步問題的經典案例。

這個問題描述了共享固定大小緩衝區的兩個程式——即所謂的“生產者”和“消費者”——在實際執行時會發生的問題。生產者的主要作用是生成一定量的資料放到緩衝區中,然後重複此過程。與此同時,消費者也在緩衝區消耗這些資料。

生產者與消費者問題的關鍵在於要保證生產者不會在緩衝區滿時加入資料,消費者也不會在緩衝區中空時消耗資料

要解決該問題,就必須讓生產者在緩衝區滿時休眠(要麼乾脆就放棄資料),等到下次消費者消耗緩衝區中的資料的時候,生產者才能被喚醒,開始往緩衝區新增資料

同樣,也可以讓消費者在緩衝區空時進入休眠,等到生產者往緩衝區新增資料之後,再喚醒消費者。通常採用執行緒間通訊的方法解決該問題,常用的方法有訊號量等。如果解決方法不夠完善,則容易出現死鎖的情況。出現死鎖時,兩個執行緒都會陷入休眠,等待對方喚醒自己。

當然,生產者與消費者問題並不是侷限於單個生產者與消費者,在實際工作中,遇到更多的是多個生產者和消費者的情形

生產者與消費者模式在軟體開發與設計中有著非常廣泛的應用。在這一模式中,生產者與消費者相互獨立,它們僅通過緩衝區傳遞資料,因此可以用於程式間的解耦非同步削峰等。

生產者與消費者問題的要點:

  • 生產者與消費者解耦,兩者通過緩衝區傳遞資料
  • 緩衝區資料裝滿了之後,生產者停止資料生產或丟棄資料
  • 緩衝區資料為空後,消費者停止消費並進入等待狀態,等待生產者通知

二、實現生產者與消費者方案

本節中,我們通過王者中的一個場景來模擬生產者與消費者問題。

在王者中,英雄蘭陵王需要通過打野來發育,但是野區的野怪在被打完之後,需要隔一段時間再投放。

所以,我們建立兩個執行緒,一個作為生產者向野區投放野怪,一個作為消費者打怪。

生產者:每秒檢查一次野區,如果野區沒有野怪,則進行投放。野怪投放後,通知打野英雄

// 野怪投放【生產者】
public static class WildMonsterProducer implements Runnable {
  public void run() {
    try {
      createWildMonster();
    } catch (InterruptedException e) {
      System.out.println("野怪投放被中斷");
    }
  }

  //投放野怪,每1秒檢查一次
  public void createWildMonster() throws InterruptedException {
    for (int i = 0;; i++) {
      synchronized(wildMonsterArea) {
        if (wildMonsterArea.size() == 0) {
          wildMonsterArea.add("野怪" + i);
          System.out.println(wildMonsterArea.getLast());
          wildMonsterArea.notify();
        }
      }
      Thread.sleep(1000);
    }
  }
}

消費者:打野英雄蘭陵王作為消費者,在野區打怪發育。如果野區有野怪,則打掉野怪。 如果沒有,會進行等待野區新的野怪產生

// 蘭陵王,打野英雄
public static class LanLingWang implements Runnable {
  public void run() {
    try {
      attackWildMonster();
    } catch (InterruptedException e) {
      System.out.println("蘭陵王打野被中斷");
    }
  }

  // 打野,如果沒有則進行等待
  public void attackWildMonster() throws InterruptedException {
    while (true) {
      synchronized(wildMonsterArea) {
        if (wildMonsterArea.size() == 0) {
          wildMonsterArea.wait();
        }
        String wildMonster = wildMonsterArea.getLast();
        wildMonsterArea.remove(wildMonster);
        System.out.println("收穫野怪:" + wildMonster);
      }
    }
  }
}

建立野區,並啟動生產者與消費者執行緒。

public class ProducerConsumerProblemDemo {

    // 野怪活動的野區
    private static final LinkedList<String> wildMonsterArea = new LinkedList<String>();

    public static void main(String[] args) {
        Thread wildMonsterProducerThread = new Thread(new WildMonsterProducer());
        Thread lanLingWangThread = new Thread(new LanLingWang());

        wildMonsterProducerThread.start();
        lanLingWangThread.start();
    }
}

在上面幾段程式碼中,你需要重點注意的是synchronizedwaitnotify用法,它們是本次方案的關鍵。執行結果如下:

野怪0
收穫野怪:野怪0
野怪1
收穫野怪:野怪1
野怪2
收穫野怪:野怪2
野怪3
收穫野怪:野怪3
野怪4
收穫野怪:野怪4
野怪5
收穫野怪:野怪5
野怪6
收穫野怪:野怪6

從結果可以看到,生產者在建立野怪後,打野英雄蘭陵王會進行打野,實現了生產者與消費者的問題。

小結

以上就是關於執行緒異常處理的全部內容,在本文中我們基於waitnotify來解決生產者與消費者問題。對於本文內容,你需要理解生產者與消費者問題的核心是什麼。另外,本文所提供的方案僅僅是這一問題多種解決方案中的一種,在後面的文章中,我們會根據新的知識點提供其他的解法。

正文到此結束,恭喜你又上了一顆星✨

夫子的試煉

  • 編寫程式碼實現生產者與消費者問題。

延伸閱讀

關於作者

關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(儘量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。

如果本文對你有幫助,歡迎點贊關注監督,我們一起從青銅到王者

相關文章