多線程設計模式解讀4—Producer-Consumer模式

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則可能在任務執行到一半時強行關閉);如果生產者和消費者數量不大,可以採用如上面示例中的毒丸對象,來關閉服務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章