
人生苦短,不如養狗
一、前言
在上一篇
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
。