天天看點

Java中的設計模式(二):生産者-消費者模式與觀察者模式

Java中的設計模式(二):生産者-消費者模式與觀察者模式
人生苦短,不如養狗

一、前言

  在上一篇

Java中的設計模式(一):觀察者模式

中我們了解了 觀察者模式 的基本原理和使用場景,在今天的這篇文章中我們要做一點簡單的延伸性學習——對比一下 生産者-消費者模式 和 觀察者模式 的異同。

二、什麼是“生産者-消費者模式”?

  和觀察者模式不同,生産者-消費者模式 本身并不屬于設計模式中的任何一種 。那麼生産者-消費者模式到底是什麼呢?下面我們用一個例子簡單說明一下:

Java中的設計模式(二):生産者-消費者模式與觀察者模式

  如同上圖中所示,生産者和消費者就如同一本雜志的投稿作者和訂閱的讀者,同一本雜志的投稿作者可以有多個,它的讀者也可以有多個,而雜志就是連接配接作者和讀者的橋梁(即緩沖區)。通過雜志這個資料緩沖區,作者可以将完成的作品投遞給訂閱了雜志的讀者,在這一過程中,作者不用關心讀者是否收到了作品或是否完成了閱讀,作者和讀者是兩個相對獨立的對象,兩者的行為互不影響。

  可以看到,在這個例子當中出現了三個角色,分别是 生産者 、 消費者 以及 緩沖區 。生産者和消費者比較好了解,前者是生産資料,後者則是處理前者生産出來的資料。而緩沖區在生産者-消費者模式中則起到了一個 解耦 、 支援異步 、 支援忙閑不均 的作用。

三、兩者的差別

1. 程式設計範式不同

  生産者-消費者模式和觀察者模式的第一個不同點在上面已經說過,前者是一種 面向過程 的軟體設計模式,不屬于Gang of Four提出的23種設計模式中的任何一種,而後者則是23中設計模式中的一種,也即面向對象的設計模式中的一種。

2. 關聯關系不同

  這一理念上的不同就帶出了下一種不同點,即觀察者模式中隻有一對多的關系,沒有多對多的關系,而在生産者-消費者模式中則是多對多的關系。

  在觀察者模式中,被觀察者隻有一個,觀察者卻可以有多個。就比如十字路口的交通燈,直行的車輛隻會觀察控制直行的交通燈,不會去觀察控制左拐或者右拐的交通燈,也就是說觀察的對象是固定唯一的。

  而在生産者-消費者模式中則不同,生産者可以有多個,消費者也可以有多個。還是用上面作者和讀者的例子,在這個例子當中,讀者隻關心雜志的内容而不必關心内容的創作者是誰,作者也隻需要知道創作完的作品可以釋出到對應的雜志,而不必關心會有那些讀者。

3. 耦合關系不同

  從上一個不同中不難看出生産者-消費者模式和觀察者模式的耦合關系也不相同,前者為 輕耦合 ,後者為 重耦合 。

4. 應用場景不同

  觀察者模式多用于 事件驅動模型 當中,生産者-消費者模式則多出現在 程序間通信 ,用于進行解耦和并發處理,我們常用的消息隊列用的就是生産者-消費者模式。當然在Java中使用生産者-消費者模式還需要注意緩沖區的線程安全問題,這裡就不做過多叙述。

四、一個小例子

  最後用一個簡單的demo來結束本次的延伸學習。

1. StoreQueue--緩沖區

public class StoreQueue<T> {
    private final BlockingQueue<T> queue = new LinkedBlockingQueue<>();
    /**
     * 隊列中增加資料
     *
     * @param data 生産者生産的資料
     */
    public void add(T data) {
        try {
            queue.put(data);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 隊列中擷取資料
     *
     * @return 從隊列中擷取到的資料
     */
    public T get() {
        try {
            return queue.take();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}      

  在這個例子中,我們使用了jdk自身的 阻塞隊列BlockingQueue 來實作了一個緩沖區,這裡隻需要實作放資料和取資料的方法。如果我們自己實作一個阻塞隊列,一方面需要注意阻塞的處理,另一方面需要考慮線程安全的問題,這裡就不展開叙述了,有興趣的同學可以看下BlockingQueue的源碼。

2. Producer--生産者

public class Producer implements Runnable{
    private StoreQueue<String> storeQueue;
    public Producer(StoreQueue<String> storeQueue) {
        this.storeQueue = storeQueue;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            storeQueue.add(Thread.currentThread().getName() + ":" + i);
        }
    }
}      

3. Consumer--消費者

public class Consumer implements Runnable{
    private StoreQueue<String> storeQueue;
    public Consumer(StoreQueue<String> storeQueue) {
        this.storeQueue = storeQueue;
    }
    @Override
    public void run() {
        try {
            while (true) {
                String data = storeQueue.get();
                System.out.println("目前消費線程 : " + Thread.currentThread().getName() + ", 接收到資料 : " + data);
            }
        } catch (Exception e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }
}      

4. 執行邏輯和運作結果

執行邏輯

public static void main(String[] args) {
        StoreQueue<String> storeQueue = new StoreQueue<>();
        Producer producer = new Producer(storeQueue);
        Consumer consumer = new Consumer(storeQueue);
        Producer producerTwo = new Producer(storeQueue);
        Consumer consumerTwo = new Consumer(storeQueue);
        new Thread(producer).start();
        new Thread(consumer).start();
        new Thread(producerTwo).start();
        new Thread(consumerTwo).start();
    }      

運作結果

目前消費線程 : Thread-1, 接收到資料 : Thread-0:0
目前消費線程 : Thread-1, 接收到資料 : Thread-0:1
目前消費線程 : Thread-1, 接收到資料 : Thread-0:2
目前消費線程 : Thread-1, 接收到資料 : Thread-0:3
目前消費線程 : Thread-1, 接收到資料 : Thread-0:4
目前消費線程 : Thread-3, 接收到資料 : Thread-0:5
目前消費線程 : Thread-3, 接收到資料 : Thread-0:7
目前消費線程 : Thread-3, 接收到資料 : Thread-0:8
目前消費線程 : Thread-3, 接收到資料 : Thread-0:9
目前消費線程 : Thread-3, 接收到資料 : Thread-2:0
目前消費線程 : Thread-3, 接收到資料 : Thread-2:1
目前消費線程 : Thread-3, 接收到資料 : Thread-2:2
目前消費線程 : Thread-3, 接收到資料 : Thread-2:3
目前消費線程 : Thread-3, 接收到資料 : Thread-2:4
目前消費線程 : Thread-3, 接收到資料 : Thread-2:5
目前消費線程 : Thread-3, 接收到資料 : Thread-2:6
目前消費線程 : Thread-3, 接收到資料 : Thread-2:7
目前消費線程 : Thread-3, 接收到資料 : Thread-2:8
目前消費線程 : Thread-3, 接收到資料 : Thread-2:9
目前消費線程 : Thread-1, 接收到資料 : Thread-0:6      

  可以看到在上面的資料結果中,不同生産者生産的資料隻會被一個消費者消費,沒有出現線程安全問題,這要歸功于實作緩沖區使用到的

BlockingQueue