併發程式設計-ExecutorCompletionService解析

京东技术發表於2024-04-12

1、簡單介紹



我們在併發程式設計中,目前大部分做法都是將任務新增到執行緒池中,並拿到Future物件,將其新增到集合中,等所有任務都新增到執行緒池後,在透過遍歷Future集合,呼叫future.get()來獲取每個任務的結果,這樣可以使得先新增到執行緒池的任務先等待其完成,但是並不能保證第一個新增到執行緒池的任務就是第一個執行完成的,所以會出現這種情況,後面新增到執行緒池的任務已經完成了,但是還必須要等待第一個任務執行完成並處理結果後才能處理接下來的任務。

如果想要不管新增到執行緒池的任務的順序,先完成的任務先進行處理,那麼就需要用到ExecutorCompletionService這個工具了。



2、原始碼解析



ExecutorCompletionService實現了CompletionService介面。CompletionService接種有有以下方法。

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;}



可以看到介面中的方法非常簡單,只有提交任務以及獲取任務結果兩類方法。

我們再看下實現類ExecutorCompletionService中的程式碼。

public class ExecutorCompletionService<V> implements CompletionService<V> {
    private final Executor executor;
    private final AbstractExecutorService aes;
    private final BlockingQueue<Future<V>> completionQueue;
    /**
     * FutureTask的子類,重寫FutureTask完成後的done方法
     */
    private class QueueingFuture extends FutureTask<Void> {
        QueueingFuture(RunnableFuture<V> task) {
            super(task, null);
            this.task = task;
        }
	// task任務執行完成後將任務放到佇列中
        protected void done() { completionQueue.add(task); }
        private final Future<V> task;
    }
    private RunnableFuture<V> newTaskFor(Callable<V> task) {
        if (aes == null)
            return new FutureTask<V>(task);
        else
            return aes.newTaskFor(task);
    }
    private RunnableFuture<V> newTaskFor(Runnable task, V result) {
        if (aes == null)
            return new FutureTask<V>(task, result);
        else
            return aes.newTaskFor(task, result);
    }
    /**
     * 構造方法,傳入一個執行緒池,建立一個佇列
     */
    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>>();
    }
    /**
     * 構造方法,傳入執行緒池和佇列
     */
    public ExecutorCompletionService(Executor executor,
                                     BlockingQueue<Future<V>> completionQueue) {
        if (executor == null || completionQueue == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        this.completionQueue = completionQueue;
    }
    // 提交一個task任務,最終將任務封裝成QueueingFuture並由指定的執行緒池執行
    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;
    }
    // 提交一個task任務,最終將任務封裝成QueueingFuture並由指定的執行緒池執行
    public Future<V> submit(Runnable task, V result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task, result);
        executor.execute(new QueueingFuture(f));
        return f;
    }
    // 從佇列中獲取執行完成的RunnableFuture物件,take方法會阻塞直到有資料
    public Future<V> take() throws InterruptedException {
        return completionQueue.take();
    }
    // 從佇列中獲取執行完成的RunnableFuture物件
    public Future<V> poll() {
        return completionQueue.poll();
    }
    // 從佇列中獲取執行完成的RunnableFuture物件
    public Future<V> poll(long timeout, TimeUnit unit)
            throws InterruptedException {
        return completionQueue.poll(timeout, unit);
    }}



透過觀察實現類中的程式碼,我們可以發現這個方法非常簡單,其原理分為以下幾步:

1、在構造ExecutorCompletionService物件時,需要傳入給定的執行緒池或者阻塞佇列。

2、當我們提交任務到ExecutorCompletionService時,會將提交的任務包裝成QueueingFuture物件,然後交由我們指定的執行緒池來執行。

3、當任務執行完成後,QueueingFuture物件會執行最終的done方法(QueueingFuture物件重新的方法),將RunnableFuture物件新增到指定的阻塞佇列中。

4、我們可以透過poll或者take方法來獲取佇列中的RunnableFuture物件,以便獲取執行結果。

由此可以發現我們獲取到的任務執行結果,與提交到執行緒池的任務順序是無關的,哪個任務先完成,就會被新增到佇列中,我們就可以先獲取執行結果。



3、使用場景



1、當我們不關注提交到執行緒池任務順序以及任務執行完成獲取結果的順序時,我們就可以使用ExecutorCompletionService這個來執行任務。以下是示例程式碼。

void solve(Executor e, Collection<Callable<Result>> solvers) throws InterruptedException, ExecutionException {
        CompletionService<Result> ecs = new ExecutorCompletionService<Result>(e);
        for (Callable<Result> s : solvers) {
            ecs.submit(s);
        }
        int n = solvers.size();
        for (int i = 0; i < n; ++i) {
            Result r = ecs.take().get();
            if (r != null) {
                use(r);
            }
        }
    }

2、當多個任務同時執行,我們只需要獲取第一個任務的執行結果,其餘結果不需要關心時,也可以透過ExecutorCompletionService來執行任務。以下是示例程式碼。

void solve(Executor e, Collection<Callable<Result>> solvers) throws InterruptedException {
        CompletionService<Result> ecs = 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(ecs.submit(s));
            }
            for (int i = 0; i < n; ++i) {
                try {
                    Result r = ecs.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);
        }
    }



4、程式碼實踐



在業務上我們有這種場景,我們有一批訂單進行批次更新,每處理完一單,我們都需要維護一下處理進度,保證訂單處理進度實時更新成最新的進度資料,我們此時用到的就是ExecutorCompletionService。

protected void parallelBatchUpdateWaybill(Map<String, LwbMain> lwbMainMap, Map<String, UpdateWaybillTaskDetail> taskDetailMap) {
        long start = System.currentTimeMillis();
        log.info("{} 並行批次更新訂單開始:{}", traceId, taskNo);
        int total = lwbMainMap.size();
        BlockingQueue<Future<String>> blockingQueue = new LinkedBlockingQueue<>(total + 2);
        ExecutorCompletionService<String> executorCompletionService = new ExecutorCompletionService<>(parallelUpdateWaybillExecutorService, blockingQueue);
        for (Map.Entry<String, UpdateWaybillTaskDetail> entry : taskDetailMap.entrySet()) {
            String lwbNo = entry.getKey();
            LwbMain lwbMain = lwbMainMap.get(lwbNo);
            UpdateWaybillTaskDetail taskDetail = entry.getValue();
            executorCompletionService.submit(() -> this.updateSingleWaybill(lwbMain, taskDetail), "done");
        }
        for (int current = 0; current < taskDetailMap.size(); current++) {
            try {
                executorCompletionService.take().get();
            } catch (Exception e) {
                log.error("{} 獲取並行批次更新訂單結果異常:{}", traceId, e.getMessage(), e);
            } finally {
                jimClient.incr(importTaskNo);
            }
        }
        long end = System.currentTimeMillis();
        log.info("{} 並行批次更新訂單結束:{},耗時:{}", traceId, taskNo, (end - start));
    }




來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70037994/viewspace-3012395/,如需轉載,請註明出處,否則將追究法律責任。

相關文章