食堂中的生產-消費模型

寒食君發表於2018-07-22

在學校的時候,我不愛去食堂成功,一是由於暗黑料理,更重要的一點是人太多了,隊伍往往從視窗排到了門口,點菜、計算價格、付款三種業務由打飯阿姨一人完成,思維切換忙碌,操作變更頻繁,導致效率低下,降低了食堂的吞吐量,造成了不好的使用者體驗。

image.png

而最近在公司食堂吃飯,發現是另外一種設計:工作人員向餐檯上新增食物,兩條通道對顧客進行分流,顧客依次進入佇列然後對餐檯上的食物進行自取,最後在佇列出口進行結賬。

變成了這樣:

image.png

在學校食堂提供的打飯服務中,是由打飯阿姨單獨完成一系列的操作。而在公司食堂,則將打飯解耦成了排盤與收銀兩個服務,降低了每個工作人員的工作複雜度,使其能更加專注於自己負責的工作。同時,在學校食堂中,一名學生在打飯的同時,排在隊伍後面的同學是出於等待狀態,等待獲取打飯阿姨的服務,這對於打飯阿姨的效能是極大的浪費。

而在公司食堂,這是一種生產-消費的模型。我們來看看都有些什麼?

首先,消費者是誰?肯定是來用餐的顧客,消費的東西是什麼?是餐檯上的食物。而生產者則是將食物擺上餐檯的工作人員。

那麼將這個場景抽象化,可以用這張圖來表示:

1532150386218.png

這張圖主要包含了這些資訊:

  • 一個資料倉儲

    用於資料快取,生產者生產完資料放入,消費者從中取出。它有兩個注意點:

  1. 同步,即生產者和消費者不能同時訪問資料倉儲。對應到現實世界,你可以理解為:顧客和工作人員不會在一個瞬間去訪問餐檯,兩個顧客也不能在同一瞬間去爭搶同一盤食物。
  2. 當倉庫滿了,生產者無法再新增資料進去,將處於阻塞狀態,並提醒消費者進行消費;當倉庫為空,消費者無法從中獲取資料。處於阻塞狀態,並提醒生產者進行生產。
  • 兩種角色:生產者、消費者。
  • 兩種關係:
  1. 依賴關係:生產者與消費者
  2. 競爭關係:生產者與生產者、消費者與消費者

那如何使用這種模型,將食堂這個場景來實現還原呢?我們可以這麼分析:

  1. 資料倉儲可以使用一個佇列或陣列來實現。但是要注意同步。我們這裡選擇java.util.concurrent包下的BlockingQueue,它有著已經實現的阻塞新增和阻塞刪除的特性。當然也可以用一個List去自己實現。
  2. 生產者是一些執行緒,生產一些資料,將其入隊。
  3. 消費者是一些執行緒,從隊首獲取資料,將其消費。

核心概念就是以上三點,那麼我們接下來用程式碼簡單實現一下。預期目標是讓整個流程自動執行起來。

首先我們來定義一個食物類Food,這裡從簡,只給它一個id屬性。

public class Food {
    private int id;
    //含參建構函式,這裡用不到無參構造器,所以可以不寫。否則新建物件將會出錯。
    public Food(int id){
        this.id=id;
    }
    //get/set
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
}
複製程式碼

接著我們來編寫生產者類Producer和消費者Consumer,讓我們來想想這兩個類該怎麼去編寫?

首先我們需要使用組合的方式來加入一條BlockingQueue(阻塞佇列),它是整個問題的核心,又由於這個容器是隻用來存放Food的,我們可以加上泛型。

之前也提到了,Food有一個屬性是id,那麼這個id從何而來呢?應該是生產者生產Food時賦予的,而由於多位生產者在同時生產,為了保證id的唯一性,我們需要對其進行原子化的自增操作。這裡我們可以使用java.util.concurrent.atomic包中的AtomicInteger,當然如果使用int,然後自己做一些同步操作也是可以的。(若這裡對Java記憶體模型JMM不瞭解的,可以去閱讀我寫過的這篇文章:)

那麼,Producer類可以這麼寫,將每個工作執行緒當作一名生產者,並作一些必要的文字輸出,方便控制檯觀察:

public class Producer implements Runnable {
    private boolean working=true;
    private BlockingQueue<Food> queue;
    private static AtomicInteger count = new AtomicInteger();
    //建構函式
    public Producer(BlockingQueue<Food> queue){
        this.queue=queue;
    }
    @Override
    public void run() {
        while(working){
            int id=count.incrementAndGet();
            Food food=new Food(id);
//            System.out.println(Thread.currentThread().getId()+"號員工開始工作");
            if(queue.offer(food)){
                System.out.println(Thread.currentThread().getId()+"號員工將"+food.getId()+"號食物加入餐檯");
            }else {
                System.out.println("餐檯已滿,"+food.getId()+"號食物無法加入");
            }
            try {
                Thread.sleep(1000*3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public  void stop(){
        working=false;
    }
}
複製程式碼

同理,Consumer可以這麼寫:

public class Consumer implements Runnable {
    private boolean working=true;
    private BlockingQueue<Food> queue;
//含參建構函式
    public Consumer(BlockingQueue<Food> queue){
        this.queue=queue;
    }
    @Override
    public void run() {
        while (true){
            try {
                Food food=queue.take();//take()方式,若佇列中沒有元素則執行緒被阻塞
                System.out.println(food.getId()+"號食物已被"+Thread.currentThread().getId()+"號顧客端走");
                Thread.sleep(1000*2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
複製程式碼

最後,就是主函式呼叫了,我們使用執行緒池來對執行緒進行分配,這裡我將資料佇列定義為10個元素空間,執行緒池使用了newFixedThreadPool方式來規定5條執行緒,初始化3名生產者和15名消費者。

Main.java

public class Main {
    public static void main(String[] args) {
        BlockingQueue<Food> queue = new LinkedBlockingDeque<>(10);
        Producer[] p=new Producer[3];
        Consumer[] c=new Consumer[15];
        for (int i=0;i<3;i++){
            p[i]=new Producer(queue);
        }
        for (int j=0;j<15;j++){
            c[j]=new Consumer(queue);
        }
        ExecutorService executorService= Executors.newFixedThreadPool(5);
        for (int i=0;i<3;i++){
            executorService.execute(p[i]);
        }
        for (int j=0;j<15;j++){
            executorService.execute(c[j]);
        }
        try {
            Thread.sleep(1000*20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

下面就是食堂開張的時刻了? 讓我們趕緊執行試試。

image.png

可以看到,生意還不錯,並且沒有出什麼問題。作為老闆(程式設計師),我們需要高負荷壓榨員工(程式效能),在現實生活中,也是老闆(真的老闆)高負荷壓榨員工(程式設計師),這條壓榨鏈條就是:老闆-->程式設計師-->計算機

有點扯遠了哈哈。繼續回到生產-消費模型,其實現在很多的店都採用了這種思想。比如你去買奶茶,都是先付款獲取一個唯一id,然後進入等待狀態,點單員執行緒繼續處理後面的消費者。接著商品製作完成,通過唯一id匹配,你獲取商品,然後再把它消費掉。

今天飯點在朋友圈看到一張圖片:

image.png

可以看得出來,一大堆執行緒都被阻塞了。

我的公眾號:位元組流。

食堂中的生產-消費模型

相關文章