☕【Java技術指南】「併發編程專題」CompletionService框架基本使用和原理探究(基礎篇)

語言: CN / TW / HK

前提概要

在開發過程中在使用多線程進行並行處理一些事情的時候,大部分場景在處理多線程並行執行任務的時候,可以通過List添加Future來獲取執行結果,有時候我們是不需要獲取任務的執行結果的,方便後面引出ExecutorCompletionService。

CompletionService的介紹

  • CompletionService 接口是一個獨立的接口,並沒有擴展ExecutorService 。 其默認實現類是ExecutorCompletionService。

  • 接口CompletionService 的功能是:以異步的方式一邊執行未完成的任務,一邊記錄、處理已完成任務的結果。從而可以將任務的執行與處理任務的執行結果分離開來。

CompletionService的實現原理

  • CompletionService就是監視着 Executor線程池執行的任務,用BlockingQueue將完成的任務的結果存儲下來。

  • 要不斷遍歷與每個任務關聯的Future,然後不斷去輪詢,判斷任務是否已經完成,功能比較繁瑣。

public interface CompletionService<V> {
    Future<V> submit(Callable<V> task);
    Future<V> submit(Runnable task, V result);
    Future<V> take() throws InterruptedException;
    Future<V> poll();
    Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
}

方法摘要

提交一個 Callable 任務;一旦完成,便可以由take()、poll()方法獲取

Future submit(Callable task):

提交一個 Runnable 任務,並指定計算結果;

Future submit(Runnable task, V result):

獲取並移除表示下一個已完成任務的 Future,如果目前不存在這樣的任務,則等待。

Future take() throws InterruptedException

獲取並移除表示下一個已完成任務的 Future,如果不存在這樣的任務,則返回 null。

Future poll()

獲取並移除表示下一個已完成任務的 Future,如果目前不存在這樣的任務,則將等待指定的時間(如果有必要)。

Future poll(long timeout, TimeUnit unit) throws InterruptedException

例子,程序提交了多個任務,但只要有一個任務完成並返回一個非空的結果,並可以忽略掉其餘的任務。

 void eample(Executor e, Collection<Callable<Result>> solvers) throws InterruptedException {
     CompletionService<Result> completionService = new ExecutorCompletionService<Result>(e);
     int n = solvers.size();
     List<Future<Result>> futures = new ArrayList<Future<Result>>(n);
     Result result = null;
     try {
         //提交多個任務
         for (Callable<Result> s : solvers)
             futures.add(completionService.submit(s));
        //
         for (int i = 0; i < n; ++i) {
             try {
                 //等待獲取一個已經完成的任務
                 Result r = completionService.take().get();
                 //判斷返回結果是否為空
                 if (r != null) {
                     result = r;
                     break;
                 }
             } catch (ExecutionException ignore) {}
         }
     }
     finally {
         //取消所有任務
         for (Future<Result> f : futures)
               f.cancel(true);
         }
     if (result != null)
         use(result);
 }

ExecutorCompletionService的介紹

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

  • ExecutorCompletionService實現了CompletionService,內部通過Executor以及BlockingQueue來實現接口提出的規範,ExecutorCompletionService,提交任務後,可以按任務返回結果的先後順序來獲取各任務執行後的結果,該類實現了接口CompletionService

構造方法

  • 指定一個Executor來執行任務,存儲完成的任務的完成隊列是LinkedBlockingQueue ;
  • Executor由調用者傳遞進來,而Blocking可以使用默認的LinkedBlockingQueue,也可以由調用者傳遞。
ExecutorCompletionService(Executor executor):

指定了任務執行器Executor和已完成的任務隊列completionQueue

ExecutorCompletionService(Executor executor, BlockingQueue<Future> completionQueue)
實現構造器
public ExecutorCompletionService(Executor executor) {
    if (executor == null)
        throw new NullPointerException();
    this.executor = executor;
    this.aes = (executor instanceof AbstractExecutorService) ?
        (AbstractExecutorService) executor : null;
    this.completionQueue = new LinkedBlockingQueue<Future<V>>();
}
  • 該接口定義了一系列方法:提交實現了Callable或Runnable接口的任務,並獲取這些任務的結果。

  • 包裝後提交任務的submit()方法,該類還會將提交的任務封裝成QueueingFuture,這樣就可以實現FutureTask.done()方法,以便於在任務執行完畢後,將結果放入阻塞隊列中。

public Future<V> submit(Callable<V> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task);
        executor.execute(new QueueingFuture(f));
        return f;
}
QueueingFuture為內部類:

在提交任務時,將任務封裝成QueueingFuture:

private class QueueingFuture extends FutureTask<Void> {
        QueueingFuture(RunnableFuture<V> task) {
            super(task, null);
            this.task = task;
        }
        protected void done() { completionQueue.add(task); }
        private final Future<V> task;
}

其中,done()方法就是在任務執行完畢後,將任務放入隊列中。

  • 在調用take()、poll()方法時,會從阻塞隊列中獲取Future對象,以取得任務執行的結果。

  • 它繼承自 FutureTask,並且重寫了 done 方法,其方法把任務放到我們包裝線程池創建的堵塞隊列裏面;就是當任務執行完成後,就會被放到隊列裏面去了。

  • 調用其take() 方法,就是阻塞等待,等到的一定是能夠獲取的結果的future,然後再調用get()方法獲取執行結果;

最後,如果工作中並行處理任務不需要獲取結果的,我們正常使用線程池提交就可以,任務技術只要適合工作的業務場景就是好的。

「其他文章」