CompletionService 與 ExecutorService 之間的區別
在討論二者之間的區別之前,先交待一下背景。
看了ElasticSearch Transport模塊的源碼,里面充滿了各種異步回調獲取結果,於是就想:為什么不用Callable接口,然后再基於java.util.concurrent.Future#get()獲取任務的執行結果呢?
又因為ES的Transport模塊底層是基於Netty實現的,研究了下Netty的獲取線程執行結果的方式,,雖然Callable、FutureTask 將提交任務執行"異步化"了,但是在獲取任務執行結果的這一步,JDK Future#get() 是阻塞的(超時阻塞),那么,能不能在獲取結果的時候也不阻塞呢?有二種渠道實現:
-
回調機制
ElasticSearch里面就是大量用到回調機制。由於JDK Future的缺陷,Netty的 ChannelFuture擴展了JDK 的Future接口,並提供了回調機制支持異步獲取任務的執行結果。它的源碼:io.netty.channel.ChannelFuture的注釋非常值得一讀。
-
JDK8 里面提供的java.util.concurrent.CompletableFuture
CompletableFuture 可參考《JAVA8實戰》中了解一下
當然,本文不打算討論,獲取任務執行結果也不阻塞的具體實現方法,而是"先退一步",來分析下:
- 使用 CompletionService 的 submit 方法 java.util.concurrent.CompletionService#submit(java.util.concurrent.Callable
)提交 多個任務,如何獲取任務的執行結果? - 使用 ExecutorService 的submit 方法 java.util.concurrent.ExecutorService#submit(java.util.concurrent.Callable
)提交 多個任務,如何獲取任務的執行結果?
為什么強調多個任務,因為這里討論的是多個任務的並發執行。並不是第一個任務執行完成后,才能執行第二個任務。那CompletionService 與 ExecutorService 在獲取任務結果的時候的區別是什么?
先說下結論,如果我們的目標是盡快處理任務的執行結果,而不是必須等到所有的任務都執行完成后,拿到所有的執行結果,才能進行下一步處理,那么使用 CompletionService 是非常有好處的。
舉個例子:一個網站要顯示10幅圖像,下載完一幅就顯示一幅,而不需要將這10幅都下載下來,再統一顯示,那就很適合用CompletionService。下載 就是線程要執行的任務,圖像 就是任務的執行結果。
使用ExecutorService時,代碼是這樣的:
//保存 Future<Image>,后面遍歷 List 獲取 Future 結果
List<Future<Image>> futureList = new ArrayList();
for(int i = 0; i < 10; i++)
{
Future<Image> imageFuture = executorService.submit(downloadTask);//10個下載任務同時並發
futureList.add(imageFuture);
}
//獲取10個任務的執行結果
for(int i = 0; i < 10; i++)
{
Future<Imapge> future = futureList.get(i);
Imapge image = future.get();//如果圖像尚未下載完成,這里會阻塞
render(image);//將已經下載好的圖像渲染到界面
}
我們是用List<Future<Image>>
保存所有的任務Future,然后在for循環里面遍歷List獲取結果,假設第一個任務下載第一幅圖像,第二個任務下載第2幅圖像,以此類推....
這里的問題是:若第一幅圖像未下載完成,但是第2幅、第3幅圖像已經下載完了,我們也無法優先獲取第2幅、第3幅圖像。也就無法將已經先下載下來的圖像渲染到界面。
總結起來講就是:提交任務,將任務添加到List里面的順序,與任務實際完成順序是不相關的。
而使用 CompletionService,就能解決這個缺陷。它使得我們能夠獲得那些最先下載好的圖像。
使用 CompletionService時,代碼是這樣的:
for(int i = 0; i < 10; i++)
{
completionService.submit(downloadTask);//10個下載任務同時並發
}
for(int i = 0; i<10;i++ )
{
//只要任一幅圖像下載下來了,completionService.take()就會返回,從而 get() 到這幅圖像
render(completionService.take().get());
}
completionService.take()
是個阻塞方法,如果10幅圖像中都沒下載下來,那就阻塞了。但只要有一幅下載下來了,就立即能獲得到這幅圖像。顯然,這里:獲取任務的執行結果的順序與提交任務的順序無關了。
這里的實現思路也可參考《Java並發編程實戰》中第6章。
參考: