java併發編程系列:生產者-消費者模式

標籤: 「咱們都是小青蛙」公衆號文章java

目光從廁所轉到飯館,一個飯館裏一般都有好多廚師以及好多服務員,這裏咱們把廚師稱爲生產者,把服務員稱爲消費者,廚師和服務員是不直接打交道的,而是在廚師作好菜以後放到窗口,服務員從窗口直接把菜端走給客人就行了,這樣會極大的提高工做效率,由於省去了生產者和消費者之間的溝通成本。從java的角度看這個事情,每個廚師就至關於一個生產者線程,每個服務員都至關於一個消費者線程,而放菜的窗口就至關於一個緩衝隊列,生產者線程不斷把生產好的東西放到緩衝隊列裏,消費者線程不斷從緩衝隊列裏取東西,畫個圖就像是這樣:程序員

image_1c216f3an1cbj1v1j1hp83o01a6a9.png-50.3kB

現實中放菜的窗口能放的菜數量是有限的,咱們假設這個窗口只能放5個菜。那麼廚師在作完菜以後須要看一下窗口是否是滿了,若是窗口已經滿了的話,就在一旁抽根菸等待,直到有服務員來取菜的時候通知一下廚師窗口有了空閒,能夠放菜了,這時廚師再把本身作的菜放到窗口上去炒下一個菜。從服務員的角度來講,若是窗口是空的,那麼也去一旁抽根菸等待,直到有廚師把菜作好了放到窗口上,而且通知他們一下,而後再把菜端走。安全

咱們先用java抽象一下多線程

public class Food {
    private static int counter = 0;

    private int i;  //表明生產的第幾個菜

    public Food() {
        i = ++counter;
    }

    @Override
    public String toString() {
        return "第" + i + "個菜";
    }
}
複製代碼

每次建立Food對象,字段i的值都會加1,表明這是建立的第幾道菜。併發

爲了故事的順利進行,咱們首先定義一個工具類:dom

class SleepUtil {

    private static Random random = new Random();
    
    public static void randomSleep() {
        try {
            Thread.sleep(random.nextInt(1000));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
複製代碼

SleepUtil的靜態方法randomSleep表明當前線程隨機休眠一秒內的時間。ide

而後咱們再用java定義一下廚師:工具

public class Cook extends Thread {

    private Queue<Food> queue;

    public Cook(Queue<Food> queue, String name) {
        super(name);
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            SleepUtil.randomSleep();    //模擬廚師炒菜時間
            Food food = new Food();
            System.out.println(getName() + " 生產了" + food);
            synchronized (queue) {
                while (queue.size() > 4) {
                    try {
                        System.out.println("隊列元素超過5個,爲:" + queue.size() + " " + getName() + "抽根菸等待中");
                        queue.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                queue.add(food);
                queue.notifyAll();
            }
        }
    }
}
複製代碼

咱們說每個廚師Cook都是一個線程,內部維護了一個名叫queue的隊列。在run方法中是一個死循環,表明不斷的生產Food。他每生產一個Food後,都要判斷queue隊列中元素的個數是否是大於4,若是大於4的話,就調用queue.wait()等待,若是不大於4的話,就把建立號的Food對象放到queue隊列中,因爲可能多個線程同時訪問queue的各個方法,因此對這段代碼用queue對象來加鎖保護。當向隊列添加完剛建立的Food對象以後,就能夠通知queue這個鎖對象關聯的等待隊列中的服務員線程們能夠繼續端菜了。學習

而後咱們再用java定義一下服務員:優化

class Waiter extends Thread {

    private Queue<Food> queue;

    public Waiter(Queue<Food> queue, String name) {
        super(name);
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            Food food;
            synchronized (queue) {
                while (queue.size() < 1) {
                    try {
                        System.out.println("隊列元素個數爲: " + queue.size() + "," + getName() + "抽根菸等待中");
                        queue.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                food = queue.remove();
                System.out.println(getName() + " 獲取到:" + food);
                queue.notifyAll();
            }

            SleepUtil.randomSleep();    //模擬服務員端菜時間
        }
    }
}
複製代碼

每一個服務員也是一個線程,和廚師同樣,都在內部維護了一個名叫queue的隊列。在run方法中是一個死循環,表明不斷的從隊列中取走Food。每次在從queue隊列中取Food對象的時候,都須要判斷一下隊列中的元素是否小於1,若是小於1的話,就調用queue.wait()等待,若是不小於1的話,也就是隊列裏有元素,就從隊列裏取走一個Food對象,而且通知與queue這個鎖對象關聯的等待隊列中的廚師線程們能夠繼續向隊列裏放入Food對象了。

在廚師和服務員線程類都定義好了以後,咱們再建立一個Restaurant類,來看看在餐館裏真實發生的事情:

public class Restaurant {

    public static void main(String[] args) {

        Queue<Food> queue = new LinkedList<>();
        new Cook(queue, "1號廚師").start();
        new Cook(queue, "2號廚師").start();
        new Cook(queue, "3號廚師").start();
        new Waiter(queue, "1號服務員").start();
        new Waiter(queue, "2號服務員").start();
        new Waiter(queue, "3號服務員").start();
    }
}
複製代碼

咱們在Restaurant中安排了3個廚師和3個服務員,你們執行一下這個程序,會發如今若是廚師生產的過快,廚師就會等待,若是服務員端菜速度過快,服務員就會等待。可是整個過程廚師和服務員是沒有任何關係的,它們是經過隊列queue實現了所謂的解耦。

這個過程雖然不是很複雜,可是使用中仍是須要注意一些問題:

  • 咱們這裏的廚師和服務員使用同一個鎖queue

    使用同一個鎖是由於對queue的操做只能用同一個鎖來保護,假設使用不一樣的鎖,廚師線程調用queue.add方法,服務員線程調用queue.remove方法,這兩個方法都不是原子操做,多線程併發執行的時候會出現不可預測的結果,因此咱們使用同一個鎖來保護對queue這個變量的操做,這一點咱們在嘮叨設計線程安全類的時候已經強調過了。

  • 廚師和服務員線程使用同一個鎖queue的後果就是廚師線程和服務員線程使用的是同一個等待隊列。

    可是同一時刻廚師線程和服務員線程不會同時在等待隊列中,由於當廚師線程在wait的時候,隊列裏的元素確定是5,此時服務員線程確定是不會wait的,可是消費的過程是被鎖對象queue保護的,因此在一個服務員線程消費了一個Food以後,就會調用notifyAll來喚醒等待隊列中的廚師線程們;當消費者線程在wait的時候,隊列裏的元素確定是0,此時廚師線程確定是不會wait的,生產的過程是被鎖對象queue保護的,因此在一個廚師線程生產了一個Food對象以後,就會調用notifyAll來喚醒等待隊列中的服務員線程們。因此同一時刻廚師線程和服務員線程不會同時在等待隊列中。

  • 在生產和消費過程,咱們都調用了SleepUtil.randomSleep();

    咱們這裏的生產者-消費者模型是把實際使用的場景進行了簡化,真正的實際場景中生產過程和消費過程通常都會很耗時,這些耗時的操做最好不要放在同步代碼塊中,這樣會形成別的線程的長時間阻塞。若是把生產過程和消費過程都放在同步代碼塊中,也就是說在一個廚師炒菜的同時不容許別的廚師炒菜,在一個服務員端菜的同時不容許別的服務員端菜,這個顯然是不合理的,你們須要注意這一點。

以上就是wait/notify機制的一個現實應用:生產者-消費者模式的一個簡介。

題外話

寫文章挺累的,有時候你以爲閱讀挺流暢的,那實際上是背後無數次修改的結果。若是你以爲不錯請幫忙轉發一下,萬分感謝~ 這裏是個人公衆號,裏邊有更多技術乾貨,時不時扯一下犢子,歡迎關注:

小冊

另外,做者還寫了一本MySQL小冊:《MySQL是怎樣運行的:從根兒上理解MySQL》的連接 。小冊的內容主要是從小白的角度出發,用比較通俗的語言講解關於MySQL進階的一些核心概念,好比記錄、索引、頁面、表空間、查詢優化、事務和鎖等,總共的字數大約是三四十萬字,配有上百幅原創插圖。主要是想下降普通程序員學習MySQL進階的難度,讓學習曲線更平滑一點~ 有在MySQL進階方面有疑惑的同窗能夠看一下:

相關文章
相關標籤/搜索