Java并发之CompletionService源码分析

一、概述

我们在并发编程中,目前大部分做法都是将任务添加到线程池中,并拿到Future对象,将其添加到集合中,等所有任务都添加到线程池后,在通过遍历Future集合,调用future.get()来获取每个任务的结果,这样可以使得先添加到线程池的任务先等待其完成,但是并不能保证第一个添加到线程池的任务就是第一个执行完成的,所以会出现这种情况,后面添加到线程池的任务已经完成了,但是还必须要等待第一个任务执行完成并处理结果后才能处理接下来的任务。

如果想要不管添加到线程池的任务的顺序,先完成的任务先进行处理,那么就需要用到ExecutorCompletionService这个工具了。

二、案例

假设我们有三个任务需要并行处理,我们可以用ExecutorService来创建线程池,然后提交任务到线程池中,并将Future对象添加到列表中,最后通过遍历Future列表,调用future.get()来获取每个任务的结果。

public class ExecutorServiceDemo {
    public static void main(String[] args) {
        // 这里只是为了方便,真正项目中不要这样创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        List<Future<String>> futureList = new ArrayList<>();
        // 提交任务并添加到列表中,减少代码重复
        futureList.add(executorService.submit(() -> executeTask("任务1", 5)));
        futureList.add(executorService.submit(() -> executeTask("任务2", 3)));
        futureList.add(executorService.submit(() -> executeTask("任务3", 1)));
        // 获取并打印任务结果
        for (Future<String> future : futureList) {
            try {
                System.out.println(future.get());
            } catch (InterruptedException | ExecutionException  e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }

    // 任务执行方法
    private static String executeTask(String taskName, int sleepSeconds) {
        System.out.println(taskName + "开始");
        try {
            TimeUnit.SECONDS.sleep(sleepSeconds);
        } catch (InterruptedException e) {
            // 处理中断异常
            e.printStackTrace();
            Thread.currentThread().interrupt(); // 重新设置中断状态
            return taskName + "执行失败";
        }
        System.out.println(taskName + "结束");
        return taskName + "执行成功";
    }
}

执行结果如下:

任务1开始
任务2开始
任务3开始
任务3结束
任务2结束
任务1结束
任务1执行成功
任务2执行成功
任务3执行成功

我们不需要获取多个任务执行的结果,就可以采用上面的实现方式去进行并行处理任务;如果我们要获取到并行处理任务的结果快慢来进行一些处理,我们就可以使用到ExecutorCompletionService来进行实现;我们来使用ExecutorCompletionService类将线程池进行包装处理下,然后进行提交任务;

public class ExecutorCompletionServiceDemo {
    public static void main(String[] args) {
        //这里只是为了方便,真正项目中不要这样创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        ExecutorCompletionService<String> completionService = new ExecutorCompletionService<>(executorService);
        completionService.submit(submitTask("任务1", 5));
        completionService.submit(submitTask("任务2", 3));
        completionService.submit(submitTask("任务3", 1));
        for (int i = 0; i < 3; i++) {
            try {
                String result = completionService.take().get();
                System.out.println(result);
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }

    private static Callable<String> submitTask(String taskName, int sleepSeconds) {
        return () -> {
            System.out.println(taskName + "开始");
            try {
                TimeUnit.SECONDS.sleep(sleepSeconds);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(taskName + "结束");
            return taskName + "执行成功";
        };
    }
}
任务1开始
任务2开始
任务3开始
任务3结束
任务3执行成功
任务2结束
任务2执行成功
任务1结束
任务1执行成功

三、源码解析

ExecutorCompletionService实现了CompletionService接口。

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

可以看到接口中的方法非常简单,只有提交任务以及获取任务结果两类方法。

3.2 ExecutorCompletionService实现类

ExecutorCompletionService实际上可以看做是ExecutorBlockingQueue的结合体。ExecutorCompletionService在接收到要执行的任务时,通过类似BlockingQueueputtake获得任务执行的结果。

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. ExecutorCompletionServicesubmit任务时,会创建一个QueueingFuture,然后将创建的QueueingFuture丢给executor,让executor完成任务的执行工作。
  2. QueueingFuture继承于FutureTask类,而FutureTask实现了两个接口RunnableFuture
    1). Runnable一般表示要执行的任务的过程,而Future则表述执行任务的结果 (或者说是任务的一个句柄,可获取结果,取消任务等)。
    2). 因此FutureTask就是一个有结果可期待的任务。FutureTask实现了run方法,我们指定此方法一般是在在工作线程(不是submit线程)执行的。
  3. FutureTask构造的时候需要一个Callable<V>参数,Callable表示一个任务的执行过程,在run方法中恰好调用了Callable.call(),也就是任务工作在工作线程中执行。
  4. 那么任务执行完了会返回结果,这个结果是要在submit线程(就是提交任务的线程)中使用的,那么如何让submit线程可以反问到呢?答案也是在FutureTask类中,我们可以看到run方法中执行任务(Callable.call())获取结果后,会掉用一个set()方法,set方法源码如下:
    protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }
    
  5. set()将获取的结果存储到FutureTask的一个outcome字段中,这个过程是同步的,所以其他线程稍后访问是可以读取到值的。
  6. ExecutorCompletionService中的完成队列中正好存储的是FutureTask的子类,当然可以调用FutureTaskget方法,FutureTaskget方法就是获取outcome值(get()方法中调用了report()方法,report中返回了outcome字段)。
  7. FutureTask中委托的任务执行完成后,会调一个done()方法,这个方法是个空方法,而其子类QueueingFuture重写了此方法,如下:
    protected void done() {
        completionQueue.add(task);
    }
    

正是在此方法中把执行完的任务放置到完成队列中的!

3.3 小结

通过观察实现类中的代码,我们可以发现这个方法非常简单,其原理分为以下几步:

  1. 在构造ExecutorCompletionService对象时,需要传入给定的线程池或者阻塞队列。
  2. 当我们提交任务到ExecutorCompletionService时,会将提交的任务包装成QueueingFuture对象,然后交由我们指定的线程池来执行。
  3. 当任务执行完成后,QueueingFuture对象会执行最终的done方法(QueueingFuture对象重新的方法),将RunnableFuture对象添加到指定的阻塞队列中。
  4. 我们可以通过poll或者take方法来获取队列中的RunnableFuture对象,以便获取执行结果。

由此可以发现我们获取到的任务执行结果,与提交到线程池的任务顺序是无关的,哪个任务先完成,就会被添加到队列中,我们就可以先获取执行结果

四、使用场景

4.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);
        }
    }
}

4.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);
    }
}

五、示例

在业务上我们有这种场景,我们有一批订单进行批量更新,每处理完一单,我们都需要维护一下处理进度,保证订单处理进度实时更新成最新的进度数据,我们此时用到的就是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));
}

六、总结

CompletionServiceJava并发包中的一个强大工具,它在处理多个异步任务时展现出了巨大的优势。通过内部维护一个阻塞队列来存储完成的任务,CompletionService使得我们可以方便地获取已完成任务的结果,这在需要按完成顺序处理结果时特别有用。不仅如此,它还减少了管理多个Future对象的复杂性,使代码更加清晰易读。

在性能方面,合理地使用CompletionService可以提高程序的响应速度和效率。尤其是在处理大量并发任务时,它能够有效地平衡任务提交和结果处理的速度,避免资源浪费和潜在的性能问题。

posted @ 2025-05-06 21:09  夏尔_717  阅读(36)  评论(0)    收藏  举报