生產者消費者問題(Producer-consumer problem)是一個多線程同步問題的經典案例。該問題描述了兩個共享固定大小緩衝區的線程——即所謂的“生產者”和“消費者”——在實際運行時會發生的問題。生產者的主要作用是生成一定量的數據放到緩衝區中,然後重複此過程。與此同時,消費者也在緩衝區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩衝區滿時加入數據,消費者也不會在緩衝區中空時消耗數據。
什麼是生產者消費者模式
如果讓生產者直接調用消費者的某個方法,那麼生產者對於消費者就會產生依賴(也就是耦合)。將來如果消費者的代碼發生變化,可能會影響到生產者。
生產者消費者模式是通過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通訊,而通過阻塞隊列來進行通訊,所以生產者生產完數據之後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列裏取,阻塞隊列就相當於一個緩衝區,平衡了生產者和消費者的處理能力。
所以說,這個阻塞隊列就是用來給生產者和消費者解耦的。縱觀大多數設計模式,都會找一個第三者出來進行解耦,如工廠模式的第三者是工廠類,模板模式的第三者是模板類。在學習一些設計模式的過程中,如果先找到這個模式的第三者,能幫助我們快速熟悉一個設計模式。
生產者消費者模式的好處
一種實用的設計模式,常用於編寫多線程或併發代碼。下面是它的一些優點:
1)簡化開發,你可以獨立地或併發的編寫消費者和生產者,它僅僅只需知道共享對象是誰。
2)生產者不需要知道誰是消費者或者有多少消費者,對消費者來說也是一樣
3)生產者和消費者可以以不同的速度執行
4)分離的消費者和生產者在功能上能寫出更簡潔、可讀、易維護的代碼
從硬件設計角度來看,其實就是硬件設計的FIFO緩衝。
來看個實際應用:
遊戲開發中,每一個客戶端連接就相當於一個獨立的線程,每個客戶端的操作相對於其它客戶端都是異步的。比如增加英雄的經驗,在邏輯處理中可能會有很多判斷,但是不管怎麼樣,到最後都是要更新數據庫的,如果所有的客戶端都同時更新數據庫,人數多的話,數據連接池很可能被佔用完,這樣導致一些更新就獲取不了連接,就沒辦法更新數據。而生產者和消費考模式很好的解決了這個問題。邏輯處理相當於生產者,把要更新的結果數據放入到隊列中,更新數據線程從隊列中出並執行更新。而且我也可以每一個data加一個標識符,把遊戲服務器中的所有需要更新的操作都放入到這個隊列中,這時候相當於有多個生產者,一個消費者線程默默更新,也可以多個消費者線程一起更新。
下面是使用阻塞隊列實現生產者消費者模式的例子:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ProducerConsumerPattern {
public static void main(String args[]){
//Creating shared object
BlockingQueue sharedQueue = new LinkedBlockingQueue();
//Creating Producer and Consumer Thread
Thread prodThread = new Thread(new Producer(sharedQueue));
Thread consThread = new Thread(new Consumer(sharedQueue));
//Starting producer and Consumer thread
prodThread.start();
consThread.start();
}
}
//Producer Class in java
class Producer implements Runnable {
private final BlockingQueue sharedQueue;
public Producer(BlockingQueue sharedQueue) {
this.sharedQueue = sharedQueue;
}
@Override
public void run() {
for(int i=0; i<10; i++){
try {
System.out.println("Produced: " + i);
sharedQueue.put(i);
} catch (InterruptedException ex) {
Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
//Consumer Class in Java
class Consumer implements Runnable{
private final BlockingQueue sharedQueue;
public Consumer (BlockingQueue sharedQueue) {
this.sharedQueue = sharedQueue;
}
@Override
public void run() {
while(true){
try {
System.out.println("Consumed: "+ sharedQueue.take());
} catch (InterruptedException ex) {
Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
Output:
Produced: 0
Produced: 1
Consumed: 0
Produced: 2
Consumed: 1
Produced: 3
Consumed: 2
Produced: 4
Consumed: 3
Produced: 5
Consumed: 4
Produced: 6
Consumed: 5
Produced: 7
Consumed: 6
Produced: 8
Consumed: 7
Produced: 9
Consumed: 8
Consumed: 9
生存着消費者模式提供了一個很好的思維模式,靈活運用它,可以得到很多變化,比如消費者消費的數據,有可能需要繼續處理,於是消費者處理完數據之後,它又要作爲生產者把數據放在新的隊列裏,交給其他消費者繼續處理。如下圖:
這裏,生產者1將消息存放在阻塞隊列1裏,消費者1從隊列裏讀消息,然後通過消息ID進行hash得到N個隊列中的一個,然後根據編號將消息存放在到不同的隊列裏,每個阻塞隊列會分配一個線程來消費阻塞隊列裏的數據。如果消費者2無法消費消息,就將消息再拋回到阻塞隊列1中,交給其他消費者處理。
其實這裏消費者1可以看成一個管理者和中轉者,負責把消息分發給消費者2、3、4。
以下是代碼;
public class MsgQueueManager implements IMsgQueue{
private static final Logger LOGGER
= LoggerFactory.getLogger(MsgQueueManager.class);
//消息總隊列
public final BlockingQueue<Message> messageQueue;
private MsgQueueManager() {
messageQueue = new LinkedTransferQueue<Message>();
}
public void put(Message msg) {
try {
messageQueue.put(msg);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public Message take() {
try {
return messageQueue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
}
//分發消息,負責把消息從大隊列塞到小隊列裏
static class DispatchMessageTask implements Runnable {
@Override
public void run() {
BlockingQueue<Message> subQueue;
for (;;) {
//如果沒有數據,則阻塞在這裏
Message msg = MsgQueueFactory.getMessageQueue().take();
//如果爲空,則表示沒有Session機器連接上來,
需要等待,直到有Session機器連接上來
while ((subQueue = getInstance().getSubQueue()) == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
//把消息放到小隊列裏
try {
subQueue.put(msg);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
//使用Hash算法均衡獲取一個子隊列。
public BlockingQueue<Message> getSubQueue() {
int errorCount = 0;
for (;;) {
if (subMsgQueues.isEmpty()) {
return null;
}
int index = (int) (System.nanoTime() % subMsgQueues.size());
try {
return subMsgQueues.get(index);
} catch (Exception e) {
//出現錯誤表示,在獲取隊列大小之後,隊列進行了一次刪除操作
LOGGER.error("獲取子隊列出現錯誤", e);
if ((++errorCount) < 3) {
continue;
}
}
}
}
//使用的時候我們只需要往總隊列裏發消息,往消息隊列裏添加一條消息
IMsgQueue messageQueue = MsgQueueFactory.getMessageQueue();
Packet msg = Packet.createPacket(Packet64FrameType.TYPE_DATA, "{}".getBytes(), (short) 1);
messageQueue.put(msg);
參考:
https://zh.wikipedia.org/wiki/%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E9%97%AE%E9%A2%98
http://www.youxijishu.com/blogs/9.html
http://www.infoq.com/cn/articles/producers-and-consumers-mode
http://canofy.iteye.com/blog/411408
https://software.intel.com/zh-cn/blogs/2014/02/28/java
http://www.uml.org.cn/zjjs/200904161.asp