用CountDownLatch提高請求處理速度

countdownlatch是java多線程包concurrent裏的一個常見工具類,經過使用它能夠藉助線程能力極大提高處理響應速度,且實現方式很是優雅。今天咱們用一個實際案例和你們來說解一下如何使用以及須要特別注意的點。java

因爲線程類的東西都比較抽象,咱們換一種講解思路,先講解決問題的案例,而後再解釋下原理。編程

假設在微服務架構中,A服務會調用B服務處理一些事情,且每處理一次業務,A可能要調用B屢次處理邏輯相同但數據不一樣的事情。爲了提高整個鏈路的處理速度,咱們天然會想到是否能夠把A調用B的各個請求組成一個批次,這樣A服務只須要調用B服務一次,等B服務處理完一塊兒返回便可,省了屢次網絡傳輸的時間。代碼以下:安全

/** * 批次請求處理服務 * @param batchRequests 批次請求對象列表 * @return */
public List<DealResult> deal(List<DealRequest> batchRequests){
  List<DealResult> resultList = new ArrayList<>();
  if(batchRequests != null){
    for(DealRequest request : batchRequests){
      //遍歷順序處理單個請求
      resultList.add(process(request));
    }
  }
  return resultList;
}
複製代碼

可是B服務順序處理批次裏每個請求的時間並無節省,假設批次裏有3個請求,一個請求平均耗時100MS,則B服務仍是要花費300MS來處理完。有什麼辦法能馬上簡單提高3倍處理速度,令總花費時間只須要100MS?到咱們的大將countdownlatch出場了!代碼以下:服務器

/** * 使用countdownlatch的批次請求處理服務 * @param batchRequests 批次請求對象列表 * @return */
public List<DealResult> countDownDeal(List<DealRequest> batchRequests){

  //定義線程安全的處理結果列表
  List<DealResult> countDownResultList = Collections.synchronizedList(new ArrayList<DealResult>());

  if(batchRequests != null){

        //定義countdownlatch線程數,有多少個請求,咱們就定義多少個
        CountDownLatch runningThreadNum = new CountDownLatch(batchRequests.size());

    for(DealRequest request : batchRequests){
      //循環遍歷請求,並實例化線程(構造函數傳入CountDownLatch類型的runningThreadNum),馬上啓動
      DealWorker dealWorker = new DealWorker(request, runningThreadNum, countDownResultList);
      new Thread(dealWorker).start();
    }

        try {
          //調用CountDownLatch的await方法則當前主線程會等待,直到CountDownLatch類型的runningThreadNum清0
          //每一個DealWorker處理完成會對runningThreadNum減1
          //若是等待1分鐘後當前主線程都等不到runningThreadNum清0,則認爲超時,直接中斷,拋出中斷異常InterruptedException
            runningThreadNum.await(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
          //此處簡化處理,非正常中斷應該拋出異常或返回錯誤結果
            return null;
        }
  }
  return countDownResultList;
}

/** * 線程請求處理類 * */
private class DealWorker implements Runnable {

      /** 正在運行的線程數 */
      private CountDownLatch  runningThreadNum;

      /**待處理請求*/
      private DealRequest request;

      /**待返回結果列表*/
      private List<DealResult> countDownResultList;

      /** * 構造函數 * @param request 待處理請求 * @param runningThreadNum 正在運行的線程數 * @param countDownResultList 待返回結果列表 */
      private DealWorker(DealRequest request, CountDownLatch runningThreadNum, List<DealResult> countDownResultList) {
        this.request = request;
        this.runningThreadNum = runningThreadNum;
        this.countDownResultList = countDownResultList;
      }

  @Override
  public void run() {
    try{
      this.countDownResultList.add(process(this.request));
    }finally{
      //當前線程處理完成,runningThreadNum線程數減1,此操做必須在finally中完成,避免處理異常後形成runningThreadNum線程數沒法清0
      this.runningThreadNum.countDown();
    }
  }
}
複製代碼

是否是很簡單?下圖和上面的代碼又作了一個對應,假設有3個請求,則啓動3個子線程DealWorker,並實例化值數等於3的CountDownLatch。每當一個子線程處理完成後,則調用countDown操做減1。主線程處於awaiting狀態,直到CountDownLatch的值數減到0,則主線程繼續resume執行。網絡

在API中是這樣描述的: 用給定的計數 初始化 CountDownLatch。因爲調用了 countDown() 方法,因此在當前計數到達零以前,await 方法會一直受阻塞。以後,會釋放全部等待的線程,await 的全部後續調用都將當即返回。這種現象只出現一次——計數沒法被重置。若是須要重置計數,請考慮使用 CyclicBarrier。多線程

經典的java併發編程實戰一書中作了更深刻的定義: CountDownLatch屬於閉鎖的範疇,閉鎖是一種同步工具類,能夠延遲線程的進度直到其到達終止狀態。閉鎖的做用至關於一扇門:在閉鎖到達結束狀態以前(上面代碼中的runningThreadNumq清0),這扇門一直是關閉的,而且沒有任何線程能經過(上面代碼中的主線程一直await),當到達結束狀態時,這扇門會打開並容許全部線程經過(上面代碼中的主線程能夠繼續執行)。當閉鎖到達結束狀態後,將不會再改變狀態,所以這扇門將永遠保持打開狀態。架構

像FutureTask,Semaphore這類在concurrent包裏的類也屬於閉鎖,不過它們和CountDownLatch的應用場景仍是有差異的,這個咱們在後面的文章裏再細說。併發

使用CountDownLatch有哪些須要注意的點

  1. 批次請求之間不能有執行順序要求,不然多個線程併發處理沒法保證請求執行順序
  2. 各線程都要操做的結果列表必須是線程安全的,好比上面代碼範例的countDownResultList
  3. 各子線程的countDown操做要在finally中執行,確保必定能夠執行
  4. 主線程的await操做須要設置超時時間,避免因子線程處理異常而長時間一直等待,若是中斷須要拋出異常或返回錯誤結果

使用CountDownLatch提升批次處理速度的問題

  1. 若是一個批次請求數不少,會瞬間佔用服務器大量線程。此時必須使用線程池,並限定最大可處理線程數量,不然服務器不穩定性會大福提高。
  2. 主線程和子線程間的數據傳輸變得困難,稍不注意會形成線程不安全的問題,且代碼可讀性有必定降低

下一篇文章咱們講講FutureTask的應用場景,謝謝!ide

相關文章
相關標籤/搜索