Producer-Consumer模式可以說是多線程設計模式之王,後期我們要講的許多模式像Thread-Pool模式,Active Object模式等都是Producer-Consumer模式的變種。Producer-Consumer模式中的生產者和消費者阻塞喚醒機制可以通過Guarded Suspension模式實現。
爲什麼要有Producer-Consumer模式呢?
1、消除了生產者與消費者之間的代碼依賴。
2、實現線程間的協調運行。生產者與消費者之間的運行速率不同,直接調用,數據處理會產生延遲,導致程序響應性下降。
這種模式我們平時應該經常接觸到,小到單體應用中ThreadPoolExecutor的編碼,大到架構實現中Kafka,RabbitMQ的使用。它由以下角色組成:
Producer:負責生成Product,傳入Channel。
Product:由Producer生成,供Consumer使用。
Channel:Producer和Consumer之間的緩衝區,用於傳遞Product。
Consumer:從Channel獲取Product使用。
這裏我們可以使用java.util.concurrent中的BlockingQueue阻塞隊列實現Channel。它可以極大地簡化編程,take操作會一直阻塞直到有可用數據,put在channel滿時也會阻塞直到有數據被消費。它有如下實現類:
1、LinkedBlockingQueue和ArrayBlockingQueue是FIFO隊列,前者基於鏈表實現,如果不特別指定,元素個數沒有最大限制,後者基於數組實現,元素個數有最大限制。
2、PriorityBlockingQueue是按優先級排序的隊列,DelayQueue是一定時間後纔可以take的隊列。
3、SynchronousQueue,沒有存儲功能,直接傳遞,因此put和take會一直阻塞,直到另一個線程準備好參與。
該模式的示例代碼如下:
public class PCTest { public static void main(String[] args) { //channel,有界阻塞隊列,容量100 BlockingQueue<String> queue = new ArrayBlockingQueue<>(50); Producer producer = new Producer(queue); Consumer consumer = new Consumer(queue); new Thread(producer).start(); new Thread(consumer).start(); System.out.println("生產者,消費者開始運行"); } } class Consumer implements Runnable{ private BlockingQueue<String> queue; public Consumer(BlockingQueue<String> q){ this.queue=q; } @Override public void run() { try{ String data; //獲取消息,收到exit後退出 while((data = queue.take()) !="exit"){ Thread.sleep(20); System.out.println("Consume: "+data); } }catch(InterruptedException e) { e.printStackTrace(); } } } class Producer implements Runnable { private BlockingQueue<String> queue; public Producer(BlockingQueue<String> q){ this.queue=q; } @Override public void run() { //生產消息 for(int i=0; i<100; i++){ String data = new String(""+i); try { Thread.sleep(i); queue.put(data); System.out.println("Produce:"+data); } catch (InterruptedException e) { e.printStackTrace(); } } //毒丸對象 String data = "exit"; try { queue.put(data); } catch (InterruptedException e) { e.printStackTrace(); } } }
1、Channel剩餘空間問題:
如果消費者消費比較慢,這就會導致Channel中的Product逐漸積壓,對此,我們可以使用有界阻塞隊列,當隊列滿時,會阻塞直到消費者消費才繼續生產Product。如果使用無界阻塞隊列,就要考慮使用一段時間後,內存不足的情況,可以採用Semaphore信號量來控制。
2、只有一個共享隊列時的鎖的競爭
如果多個消費者同時消費同一個隊列的時候,就會導致鎖的競爭,不過BlockingQueue阻塞隊列已經幫我們實現了相應的機制,使用Lock,Condition等控制多線程運行,其實就是對Guarded Suspension模式的應用。我們可以通過工作密取算法降低鎖的競爭,提高可伸縮性。即每個消費者都有自己的雙端隊列(Deque,具體實現有ArrayDeque和LinkedBlockingDeque),一個消費者處理完自己隊列的Product時,可以從其他消費者雙端隊列的末尾祕密獲取Product。它非常適用於既是生產者又是消費者的問題,比如爬蟲,當處理一個頁面後,發現有更多頁面需要處理,把這些新任務放到自己隊列的末尾,當自己的雙端隊列爲空時,則從其他隊列尾部獲取新任務。
3、線程停止
消費者線程和生產者線程哪個先停止,一般是先停止生產者,等Channel剩餘Product備份後,或者被消費者處理完後,再停止消費者。至於具體實現,我們可以採用Two-phase termination 模式,設置停止標誌並且使用中斷;如果你使用線程池管理,則可以調用shutdown方法,它會等隊列中的所有任務完成再關閉(shuwdownNow則可能在任務執行到一半時強行關閉);如果生產者和消費者數量不大,可以採用如上面示例中的毒丸對象,來關閉服務。