Java-concurrent之CompletionService

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lqzkcx3/article/details/82668109

为什么在有了Java8的CompletableFuture之后,本人需要先研究这个CompletionService<T>;还能是为什么,当然是他喵的因为公司死活不愿意升级JDK的版本,一股抱着JDK7到死的慷慨之气真是让人动容。

1. 概述

  1. CompletionService类整合了ExecutorBlockingQueue的功能;
  2. 你可以将Callable<T>任务提交给它去执行,完成的任务被放入到一个阻塞队列中(先完成的任务先进入该队列);然后使用类似于队列中的take方法即可获取线程的返回值;
  3. 使用CompletionService可以让我们更加方便在多个任务执行后获取到这批任务的执行结果。
  4. 使用CompletionService来维护处理线程的返回结果时,主线程总是能够拿到最先完成的任务的返回值,而不管它们加入线程池的顺序。

2. 源码分析

  1. 在 JDK 中,CompletionService接口只有一个实现类 ExecutorCompletionService——该类使用创建时外界提供的 Executor 对象(通常是线程池)来执行任务,然后将结果放入一个自身提供的阻塞队列中。
  2. ExecutorCompletionService统一了ExecutorServiceBlockingQueue,既有线程池功能,能提交任务;又有阻塞队列功能,能判断所有线程的执行结果。
  3. ExecutorCompletionService的实现是维护一个保存Future对象的 BlockingQueue。只有当这个Future对象状态是结束的时候,才会加入到这个Queue中,take()方法其实就是Producer- Consumer中的Consumer。它会从Queue中取出Future对象,如果Queue是空的,就会阻塞在那里,直到有完成的Future对象 加入到Queue中。
/*
1. ExecutorCompletionService<T>的实现相当简洁,正如上面所介绍了,其只负责维护一个完成执行的Task的阻塞队列(按照Task完成的先后顺序),Task的调度执行被委托给了内部的`Executor` 字段。
2. 注释里还给了一个简单的例子, 读者可以自行查看(为了避免骗篇幅之嫌,省略了相关的注释)
*/

public class ExecutorCompletionService<V> implements CompletionService<V> {
    //继承链 Executor > ExecutorService > AbstractExecutorService 
    private final Executor executor;
    private final AbstractExecutorService aes;
    // 这就是上面一直提到的阻塞队列
    private final BlockingQueue<Future<V>> completionQueue;

    // JDK提供了两个构造函数, 这里我们选取使用频率最高的一个
    public ExecutorCompletionService(Executor executor) {
        if (executor == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        // 构造一个阻塞队列; 看字段名称我们可以大致猜测其中存放的是已完成的task任务。
        // 也是实现的关键所在
        this.completionQueue = new LinkedBlockingQueue<Future<V>>();
    }

    // ----- 提交任务去异步执行
    public Future<V> submit(Callable<V> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task);
        // 包裹为本类的内部类QueueingFuture, 提交给Executor去执行
        executor.execute(new QueueingFuture(f));
        return f;
    }

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

    // ----- 直接调度给底层queue的同名方法
    // 获取并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则等待。(如果需要用到返回值建议用take)
    public Future<V> take() throws InterruptedException {
        return completionQueue.take();
    }

    // 获取并移除表示下一个已完成任务的 Future,如果不存在这样的任务,则返回null。
    public Future<V> poll() {
        return completionQueue.poll();
    }

    public Future<V> poll(long timeout, TimeUnit unit)
            throws InterruptedException {
        return completionQueue.poll(timeout, unit);
    }

    // ----- 区分 ExecutorService 和 Executor 来构造RunnableFuture实例
    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);
    }

    // ----- 内部类
    /**
     * FutureTask extension to enqueue upon completion
     */ 
    private class QueueingFuture extends FutureTask<Void> {
        QueueingFuture(RunnableFuture<V> task) {
            super(task, null);
            this.task = task;
        }
        // ----- 关键点
        // 在Task完成时将该task推入到Queue。(所以说调用take()取出来的task是按照task完成的时间顺序)
        protected void done() { completionQueue.add(task); }
        private final Future<V> task;
    }

}

大致的逻辑如下图:
内部逻辑图

3. 范例

直接上代码

//初始化线程池 
ExecutorService threadPool = Executors.newFixedThreadPool(10);
CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(threadPool);

for (int j = 1; j <= 5; j++) {
    final int index = j;
    // 提交任务
    completionService.submit(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            java.lang.Thread.sleep(1000l * (5 - index));
            return index;
        }
    });
}

// 关闭线程池, 不再接收引新的任务 
threadPool.shutdown();

// 获取执行结果
for (int i = 0; i < 5; i++) {
    try {
        // 线程执行结束的顺序就是获取结果的顺序。
        System.out.println("线程:" + completionService.take().get() + " 任务执行结束:");
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

通过控制台的输出可以很清晰地展示: 取值顺序和提交Task的顺序正好相反。

4. 总结

最后我们尝试总结下CompletionService相关的知识点:
1. JDK5开始提供的CompletionService统一了ExecutorServiceBlockingQueue,简化批量任务的执行与相应结果的获取。
2. ExecutorCompletionService的实现中,任务的执行是委托给了Executor,自身只负责将执行完毕的任务按照完成时间上的先后顺序收集到阻塞队列中,外界通过CompletionService接口提供的take()/poll()方法按顺序获取相应的返回值。
3. 注意ExecutorCompletionService执行的批量任务不能有时间上的先后顺序约束。
4. 相比ExecutorServiceCompletionService可以更精确和简便地完成异步任务的执行。
5. 在执行大量相互独立和同构的任务时,可以使用CompletionService

  1. happen-before 《Java高并发程序设计》P27
  2. CompletionService异步
  3. CompletionService简介

猜你喜欢

转载自blog.csdn.net/lqzkcx3/article/details/82668109