高併發編程-ExecutorCompletionService深入解析

要點解說

假設現在有一大批需要進行計算的任務,爲了提高整批任務的執行效率,你可能會使用線程池,向線程池中不斷submit異步計算任務,同時你需要保留與每個任務關聯的Future,最後遍歷這些Future,通過調用Future接口實現類的get方法獲取整批計算任務的各個結果。

雖然使用了線程池提高了整體的執行效率,但遍歷這些Future,調用Future接口實現類的get方法是阻塞的,也就是和當前這個Future關聯的計算任務真正執行完成的時候,get方法才返回結果,如果當前計算任務沒有執行完成,而有其它Future關聯的計算任務已經執行完成了,就會白白浪費很多等待的時間,所以最好是遍歷的時候誰先執行完成就先獲取哪個結果,這樣就節省了很多持續等待的時間。

而ExecutorCompletionService可以實現這樣的效果,它的內部有一個先進先出的阻塞隊列,用於保存已經執行完成的Future,通過調用它的take方法或poll方法可以獲取到一個已經執行完成的Future,進而通過調用Future接口實現類的get方法獲取最終的結果。

實例演示

@Test	
public void test() throws InterruptedException, ExecutionException {	
    Executor executor = Executors.newFixedThreadPool(3);	
    CompletionService<String> service = new ExecutorCompletionService<>(executor);	
    for (int i = 0 ; i < 5 ;i++) {	
        int seqNo = i;	
        service.submit(new Callable<String>() {	
            @Override	
            public String call() throws Exception {	
                return "HelloWorld-" + seqNo + "-" + Thread.currentThread().getName();	
            }	
        });	
    }	
    for (int j = 0 ; j < 5; j++) {	
        System.out.println(service.take().get());	
    }	
}

執行結果:

HelloWorld-2-pool-1-thread-3	
HelloWorld-1-pool-1-thread-2	
HelloWorld-3-pool-1-thread-2	
HelloWorld-4-pool-1-thread-3	
HelloWorld-0-pool-1-thread-1 

方法解析

ExecutorCompletionService實現了CompletionService接口,在CompletionService接口中定義瞭如下這些方法:

  • Future<V> submit(Callable<V> task):提交一個Callable類型任務,並返回該任務執行結果關聯的Future;

  • Future<V> submit(Runnable task,V result):提交一個Runnable類型任務,並返回該任務執行結果關聯的Future;

  • Future<V> take():從內部阻塞隊列中獲取並移除第一個執行完成的任務,阻塞,直到有任務完成;

  • Future<V> poll():從內部阻塞隊列中獲取並移除第一個執行完成的任務,獲取不到則返回null,不阻塞;

  • Future<V> poll(long timeout, TimeUnit unit):從內部阻塞隊列中獲取並移除第一個執行完成的任務,阻塞時間爲timeout,獲取不到則返回null;

源碼解析

根據上面的實例演示代碼分析ExecutorCompletionService內部的實現原理。

ExecutorCompletionService有三個私有屬性,分別是executor、aes和completionQueue,其中completionQueue就是存儲已完成任務的隊列,具體代碼如下圖。

640?wx_fmt=png

進入它的構造方法,在方法內部給它的三個屬性賦值,可以看到在這裏初始化了一個LinkedBlockingQueue類型的先進先出阻塞隊列,具體代碼如下圖。

640?wx_fmt=png

接着,進入ExecutorCompletionService的submit方法,這裏我們分析參數類型是Callable的submit方法,具體代碼如下圖。

640?wx_fmt=png

跟蹤代碼進入newTaskFor方法,具體代碼如下圖。

640?wx_fmt=png

ExecutorCompletionService構造方法中已經給aes賦過值了,所以進入AbstractExecutorService的newTaskFor方法,具體代碼如下圖。

640?wx_fmt=png

跟蹤代碼進入FutureTask構造方法,具體代碼如下圖。

640?wx_fmt=png

到這裏構建的RunnableFuture實例對象完成了,回到上述的submit方法中,繼續分析executor.execute(new QueueingFuture(f)),首先是new QueueingFuture(f),QueueingFuture是ExecutorCompletionService中的內部類,具體代碼如下圖。

640?wx_fmt=png

從圖中的代碼可以看到,將RunnableFuture實例對象賦值給了QueueingFuture的task屬性,注意上圖紅框中有一個done方法,它的內部是將一個task添加到已完成阻塞隊列中,這個先記住後面會用到。接着,分析executor.execute(new QueueingFuture(f)),因爲我們的實例演示代碼中使用到的是ThreadPoolExecutor,所以executor.execute()方法執行到ThreadPoolExecutor中,具體重點代碼如下圖。640?wx_fmt=png

640?wx_fmt=png

這裏我們不分析極端的情況,當工作線程數小於核心線程數的時候,執行addWorker方法,這個方法體的內容比較多,這裏只關注重點代碼,具體代碼如下圖。

640?wx_fmt=png

第一個紅框中的代碼會構建一個Worker實例,具體代碼如下圖。

640?wx_fmt=png

根據上圖中的紅框代碼,繼續跟蹤代碼,會發現t.start()方法會執行到上圖的run方法中,而run方法的內部執行了runWorker方法,具體代碼如下圖。

640?wx_fmt=png

上圖中代碼繼續跟蹤可以發現,執行task.run()會進入前面構建的RunnableFuture實例對象的run方法中,具體代碼如下圖。

640?wx_fmt=png

第一個紅框中的代碼就是實際任務執行的代碼,也就是submit提交的任務真正執行的地方。第二個紅框中的代碼是當發生異常時的處理,第三個紅框中的代碼是正常執行完成的處理,下面是它們的具體實現代碼。

640?wx_fmt=png

640?wx_fmt=png

從上面兩張圖中的代碼發現,都執行了finishCompletion()方法,下面來揭曉這個方法的作用,具體代碼如下圖。

640?wx_fmt=png

從上圖紅框中的代碼可以看到,這裏執行了done()方法,實際執行的是我們前面分析提到的將一個task添加到已完成阻塞隊列中的那個done方法。至此,當一個任務執行完成或異常的時候,都會被添加到已完成阻塞隊列中,進而被取出處理。

下面再分析一下ExecutorCompletionService中的take方法和poll方法,具體代碼如下圖。

640?wx_fmt=png

640?wx_fmt=png

從上圖可以看到,都是操作已完成阻塞隊列,那我們就看一下這個已完成阻塞列隊中的代碼,如下圖。

640?wx_fmt=png

上圖清晰的展示了通過循環等待已完成的執行任務。

640?wx_fmt=png

上圖代碼不阻塞,當沒有已完成的執行任務時,直接返回null。

640?wx_fmt=png

上圖代碼阻塞指定時間,當沒有已完成的執行任務時,直接返回null。

往期精彩內容

  • 不斷更新...


覺得有收穫,誠邀關注、點贊、轉發

640?wx_fmt=png

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