更好的异步执行器:CompletionService 和 ExecutorCompletionService 源码分析

1、继承关系图

 

2、CompletionService

2.1、简介

关于 Future 接口,具体见:线程池系列 之 FutureTask 及其底层实现

CompletionService与ExecutorService类似都可以用来执行线程池的任务,ExecutorService继承了Executor接口,而CompletionService则是一个接口,那么为什么CompletionService不直接继承Executor接口呢?

主要是Executor的特性决定的:

Executor框架不能完全保证任务执行的异步性,那就是如果需要实现任务(task)的异步性,只要为每个task创建一个线程就实现了任务的异步性。代码往往包含new Thread(task).start()。这种方式的问题在于,它没有限制可创建线程的数量(在ExecutorService可以限制),不过,这样最大的问题是在高并发的情况下,不断创建线程异步执行任务将会极大增大线程创建的开销、造成极大的资源消耗和影响系统的稳定性。另外,Executor框架还支持同步任务的执行,就是在execute方法中调用提交任务的run()方法就属于同步调用

一般情况下,如果需要判断任务是否完成,思路是得到Future列表的每个Future,然后反复调用其get方法,并将timeout参数设为0,从而通过轮询的方式判断任务是否完成。为了更精确实现任务的异步执行以及更简便的完成任务的异步执行,可以使用CompletionService

使用CompletionService的一大改进就是把多个图片的加载分发给多个工作单元进行处理,这样通过分发的方式就缩小了商品图片的加载与简介信息的加载的速度之间的差距,让这些小任务在线程池中执行,这样就大大降低了下载所有图片的时间,所以在这个时候可以认为这两个任务是同构的。使用CompletionService完成最合适不过了。

 

2.2、CompletionService实现原理

CompletionService实际上可以看做是Executor和BlockingQueue的结合体。CompletionService在接收到要执行的任务时,通过类似BlockingQueue的put和take获得任务执行的结果。CompletionService的一个实现是ExecutorCompletionService,ExecutorCompletionService把具体的计算任务交给Executor完成

在实现上,ExecutorCompletionService在构造函数中会创建一个BlockingQueue(使用的基于链表的无界队列LinkedBlockingQueue),该BlockingQueue的作用是保存Executor执行的结果。当计算完成时,调用FutureTask的done方法。当提交一个任务到ExecutorCompletionService时,首先将任务包装成QueueingFuture,它是FutureTask的一个子类,然后改写FutureTask的done方法,之后把Executor执行的计算结果放入BlockingQueue中。

从代码可以看到,CompletionService将提交的任务转化为QueueingFuture,并且覆盖了done方法,在done方法中就是将任务加入任务队列中。这点与之前对Executor框架的分析是一致的。

 

2.3、接口 源码

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

 

2.4、CompletionService小结

  1. 相比ExecutorService,CompletionService可以更精确和简便地完成异步任务的执行
  2. CompletionService的一个实现是ExecutorCompletionService,它是Executor和BlockingQueue功能的融合体,Executor完成计算任务,BlockingQueue负责保存异步任务的执行结果
  3. 执行大量相互独立和同构的任务时,可以使用CompletionService
  4. CompletionService可以为任务的执行设置时限,主要是通过BlockingQueue的poll(long time,TimeUnit unit)为任务执行结果的取得限制时间,如果没有完成就取消任务

关于 ExecutorService 接口,具体见:Executor框架底层:Executor、ExecutorService、AbstractExecutorService 和 ScheduledExecutorService

 

3、ExecutorCompletionService 类

3.1、简介

ExecutorCompletionService类是CompletionService接口的实现

ExecutorCompletionService可以很好的配合线程池使用,它的内部封装了线程池(线程池需要在构造对象时传入),将提交的任务代理给线程池执行(但任务已经不再是FutureTask类型,而是FutureTask的子类QueueingFuture,QueueingFuture重写了done()方法,该方法在FutureTask类中是空实现),因为提交的任务被转换为QueueingFuture对象,该对象在任务处理完成之后,会主动将该任务放到ExecutorCompletionService维护的阻塞队列中,因此执行完成的任务都会被放到阻塞队列中,使用结果时,只需调用take()或者poll()方法获取即可。

  • ExecutorCompletionService内部管理者一个已完成任务的阻塞队列
  • ExecutorCompletionService引用了一个Executor, 用来执行任务
  • submit()方法最终会委托给内部的executor去执行任务
  • take/poll方法的工作都委托给内部的完成任务阻塞队列
  • 如果阻塞队列中有已完成的任务, take方法就返回任务的结果, 否则阻塞等待任务完成
  • poll与take方法不同, poll有两个版本:
    • 无参的poll方法 --- 如果完成队列中有数据就返回, 否则返回null
    • 有参数的poll方法 --- 如果完成队列中有数据就直接返回, 否则等待指定的时间, 到时间后如果还是没有数据就返回null
    • ExecutorCompletionService主要用与管理异步任务 (有结果的任务, 任务完成后要处理结果)

 

3.2、ExecutorCompletionService 实现原理

(1)内部一个阻塞队列,存放已经完成的任务,这样通过 take 和 poll 从阻塞队列中取执行结果

AbstractExecutorService 里面就没有阻塞队列,也没有Executor;只是invokeAny里面引用了ExecutorCompletionService

(2)内部包含一个 线程池,用于执行任务(在构造方法时注入)

(3)内部类 QueueingFuture extends FutureTask<Void> ,主要是重写了 done() 方法(原本为空),将 task 放入结果的阻塞队列中

(4)AbstractExecutorService --> ase ,主要是用于封装任务 和 线程池

(5)是Executor和BlockingQueue功能的融合体,Executor完成计算任务,BlockingQueue负责保存异步任务的执行结果

 

3.3、成员变量

public class ExecutorCompletionService<V> implements CompletionService<V> {

    // 执行任务的线程池
    private final Executor executor;

    // 内部线程池,传进来的具体参数是 AbstractExecutorService 的实现类
    private final AbstractExecutorService aes;

    // 存放已完成(!!!)的异步任务的阻塞队列,默认使用 LinkedBlockingQueue
    private final BlockingQueue<Future<V>> completionQueue;
}

关于 AbstractExecutorService 抽象类,具体见:Executor框架底层:Executor、ExecutorService、AbstractExecutorService 和 ScheduledExecutorService

 

3.4、构造方法

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

 

3.5、内部类

QueueingFuture 继承自 FutureTask,并重写了其中的 done() 方法:

原本是空方法;现在是将 任务加入 完成队列中(阻塞队列)

注意:FuturnTask 中也有一个栈(等待线程等装成的节点waiters,是volatile;对应的内部成员是private volatile WaitNode waiters;)

private class QueueingFuture extends FutureTask<Void> {

    // 任务
    private final Future<V> task;

    // 构造方法
    QueueingFuture(RunnableFuture<V> task) {
        super(task, null);
        this.task = task;
    }

    // 就是将任务添加到 阻塞队列(任务队列)中
    protected void done() { completionQueue.add(task); }
}

 

3.6、方法

(1)封装任务 newTaskFor

将 Callable 或 Runnable+result 封装成 FuturnTask

newTaskFor(Callable<V> task)

private RunnableFuture<V> newTaskFor(Callable<V> task) {
    if (aes == null)
        return new FutureTask<V>(task);
    else
        return aes.newTaskFor(task);  
    //AbstractExecutorService的newTaskFor也是封装成FuturnTask,其子类有没有覆盖此方法不知道
}

 newTaskFor(Runnable task, V result) 

private RunnableFuture<V> newTaskFor(Runnable task, V result) {
    if (aes == null)
        return new FutureTask<V>(task, result);
    else
        return aes.newTaskFor(task, result);
}

 

(2)submit 系列

我们可以看到,在提交任务给线程池之前,我们会将任务封装成QueueingFuture任务。当该任务执行(executor.execute)完成后会回调执行done方法,将任务放到队列

AbstractExecutorService 是将任务封装成FuturnTask任务,任务执行完之后,状态改变,然后唤醒等待在 get 方法上的线程。FuturnTask 没有结果等待队列,只有一个线程等待链表

submit(Callable<V> task)  

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

submit(Runnable task, V result) 

public Future<V> submit(Runnable task, V result) {
    if (task == null) 
        throw new NullPointerException();

    RunnableFuture<V> f = newTaskFor(task, result);  //封装任务和result
    executor.execute(new QueueingFuture(f));
    return f;
}

 

(3)take() 

completionQueue为阻塞队列。阻塞队列的take()是阻塞方法。

如果 完成队列中有数据,那么直接返回;如果没有数据,就阻塞当前线程(等到QueueingFuturn任务状态编程完成),返回。

public Future<V> take() throws InterruptedException {
    return completionQueue.take();
}

 

 (4)poll 系列

阻塞队列的poll() 方法是立刻返回

如果完成队列没有数据,那么返回null;如果有数据,就返回数据。总之是立刻返回

 poll() 

public Future<V> poll() {
    return completionQueue.poll();
}

poll(long timeout, TimeUnit unit)  

如果完成队列中有数据就直接返回, 否则等待指定的时间, 到时间后如果还是没有数据就返回null

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

 

4、为什么 ExecutorCompletionService 比 AbstractExecutorService 并发度更高?

假设现在有一大批需要进行计算的任务,为了提高整批任务的执行效率,你可能会使用线程池,向线程池中不断submit异步计算任务,同时你需要保留与每个任务关联的Future,最后遍历这些Future,通过调用Future接口实现类的get方法获取整批计算任务的各个结果。

虽然使用了线程池提高了整体的执行效率,但遍历这些Future,调用Future接口实现类的get方法是阻塞的,也就是和当前这个Future关联的计算任务真正执行完成的时候,get方法才返回结果,如果当前计算任务没有执行完成,而有其它Future关联的计算任务已经执行完成了,就会白白浪费很多等待的时间,所以最好是遍历的时候谁先执行完成就先获取哪个结果,这样就节省了很多持续等待的时间。

ExecutorCompletionService可以实现这样的效果,它的内部有一个先进先出的阻塞队列,用于保存已经执行完成的Future,通过调用它的take方法或poll方法可以获取到一个已经执行完成的Future,进而通过调用Future接口实现类的get方法获取最终的结果

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值