分享一個JDK批量非同步任務工具CompletionService,超好用

華為雲開發者社群發表於2022-04-27
摘要:當需要批量提交非同步任務,推薦CompletionService。CompletionService將執行緒池Executor和阻塞佇列融合,讓批量非同步任務管理更簡單。

本文分享自華為雲社群《JDK批量非同步任務最強工具CompletionService》,作者: JavaEdge。

如何優化一個查詢各個價格介面的程式碼?若使用“ThreadPoolExecutor+Future”,可能優化如下:

三個執行緒非同步執行查詢價格,通過三次呼叫Future的get()方法獲取結果,之後將查詢結果儲存在MySQL。

分享一個JDK批量非同步任務工具CompletionService,超好用

若獲取price1耗時很長,那麼即便獲取price2耗時短,也無法讓儲存price2的操作先執行,因為主執行緒都阻塞在 f1.get()。這種問題如何解決呢?

加個阻塞佇列! 獲取到price1、2、3都進入阻塞佇列,然後在主執行緒消費阻塞佇列,就能保證先獲取到的價格先儲存:

分享一個JDK批量非同步任務工具CompletionService,超好用

CompletionService實現查詢價格

實際開發推薦CompletionService,不但能幫你解決先獲取到的價格先儲存,還能精簡程式碼。

CompletionService內部維護了一個阻塞佇列,當任務執行結束就把任務的執行結果入隊,但CompletionService是把任務執行結果的Future物件入隊,而上面demo是把任務最終執行結果入隊。

建立CompletionService

CompletionService介面的實現類是ExecutorCompletionService,這個實現類的構造方法有兩個,分別是:

  • ExecutorCompletionService(Executor executor);
分享一個JDK批量非同步任務工具CompletionService,超好用
  • ExecutorCompletionService(Executor executor, BlockingQueue<Future<V>> completionQueue)
分享一個JDK批量非同步任務工具CompletionService,超好用

這倆構造器都需要傳入一個執行緒池,若不指定completionQueue,預設使用無界LinkedBlockingQueue。任務執行結果的Future物件就是加入到completionQueue中。

學長~你直接給我寫段程式碼解釋清楚點唄 ~

讓我們試著利用CompletionService實現高效能的查詢房價系統。 之後通過CompletionService#submit()提交三個詢價操作,這三個詢價操作將會被CompletionService非同步執行。

最後CompletionService#take()獲取一個Future物件(加入到阻塞佇列的是任務執行結果的Future物件),呼叫Future#get()就能返回執行結果。

分享一個JDK批量非同步任務工具CompletionService,超好用

CompletionService介面

CompletionService介面提供的方法

分享一個JDK批量非同步任務工具CompletionService,超好用

submit()相關的方法有兩個:

  • 一個方法引數是Callable<V> task
  • 一個方法有兩個引數,分別是Runnable task和V result,該方法類似於ThreadPoolExecutor的 <T> Future<T> submit(Runnable task, T result) ,

CompletionService實現Dubbo#Forking Cluster

Dubbo中有一種叫做Forking的叢集模式,這種叢集模式下,支援並行呼叫多個查詢服務,只要有一個成功返回結果,整個服務即可返回。例如你需要提供一個地址轉座標的服務,為了保證該服務的高可用和效能,可並行呼叫3個地圖服務商的API,然後只要有1個正確返回了結果r,那麼地址轉座標這個服務就可以直接返回r了。這種叢集模式可以容忍2個地圖服務商服務異常,但缺點是消耗的資源偏多。

geocoder(addr) {
   // 並行執行以下3個查詢服務, 
   r1=geocoderByS1(addr);
   r2=geocoderByS2(addr);
   r3=geocoderByS3(addr);
   // 只要r1,r2,r3有一個返回
   // 則返回
   return r1|r2|r3;
 }

利用CompletionService可快速實現 Forking 這種叢集模式,比如下面示例程式碼。 首先建立一個執行緒池executor 、一個CompletionService物件cs和一個Future<Integer>型別的列表 futures,每次通過呼叫CompletionService的submit()方法提交一個非同步任務,會返回一個Future物件,把這些Future物件儲存在列表futures中。通過呼叫 cs.take().get(),我們能夠拿到最快返回的任務執行結果,只要我們拿到一個正確返回的結果,就可以取消所有任務並且返回最終結果了。

 // 建立執行緒池
 ExecutorService executor =
   Executors.newFixedThreadPool(3);
 // 建立CompletionService
 CompletionService<Integer> cs =
   new ExecutorCompletionService<>(executor);
 // 用於儲存Future物件
 List<Future<Integer>> futures =
   new ArrayList<>(3);
 // 提交非同步任務,並儲存future到futures 
 futures.add(
   cs.submit(()->geocoderByS1()));
 futures.add(
   cs.submit(()->geocoderByS2()));
 futures.add(
   cs.submit(()->geocoderByS3()));
 // 獲取最快返回的任務執行結果
 Integer r = 0;
 try {
   // 只要有一個成功返回,則break
   for (int i = 0; i < 3; ++i) {
     r = cs.take().get();
     // 簡單地通過判空來檢查是否成功返回
     if (r != null) {
       break;
     }
   }
 } finally {
   // 取消所有任務
   for(Future<Integer> f : futures)
     f.cancel(true);
 }
 // 返回結果
 return r;

總結

當需要批量提交非同步任務,推薦CompletionService。CompletionService將執行緒池Executor和阻塞佇列融合,讓批量非同步任務管理更簡單。

CompletionService能讓非同步任務的執行結果有序化,先執行完的先進入阻塞佇列,利用該特性,可以輕鬆實現後續處理的有序性,避免無謂等待,同時還可以快速實現諸如Forking Cluster這樣的需求。

CompletionService的實現類ExecutorCompletionService,需要你自己建立執行緒池,雖看上去有些囉嗦,但好處是你可以讓多個ExecutorCompletionService的執行緒池隔離,這種隔離效能避免幾個特別耗時的任務拖垮整個應用的風險。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章