前言
文章
- 相关系列:《Java ~ Executor【目录】》(持续更新)
- 相关系列:《Java ~ Executor ~ ExecutorCompletionService【源码】》(学习过程/多有漏误/仅作参考/不再更新)
- 相关系列:《Java ~ Executor ~ ExecutorCompletionService【总结】》(学习总结/最新最准/持续更新)
- 相关系列:《Java ~ Executor ~ ExecutorCompletionService【问题】》(学习解答/持续更新)
- 涉及内容:《Java ~ Executor【总结】》
- 涉及内容:《Java ~ Executor ~ AbstractExecutorService【总结】》
- 涉及内容:《Java ~ Executor ~ CompletionService【总结】》
- 涉及内容:《Java ~ Executor ~ Future【总结】》
- 涉及内容:《Java ~ Executor ~ RunnableFuture【总结】》
- 涉及内容:《Java ~ Executor ~ FutureTask【总结】》
- 涉及内容:《Java ~ Executor ~ Callable【总结】》
- 涉及内容:《Java ~ Collection/Executor ~ PriorityBlockingQueue【总结】》
- 涉及内容:《Java ~ Collection ~ PriorityQueue【总结】》
- 涉及内容:《Java ~ Thread ~ Runnable【总结】》
一 概述
简介
任务的“生产”与“消费”默认是同步的。在正式讲述ExecutorCompletionService(执行器完成服务)类的作用之前我们需要先了解所谓任务的“生产”与“消费”是什么概念。在Executor(执行器)框架中任务的“生产”指任务向执行器递交的行为,而“消费”则指将任务从执行器中移除的行为,即执行器不再持有任务的引用,因此“消费”只能在任务执行结束后(完成/异常/取消)后发生。任务的“生产”与“消费”通常被认为是同步的,这是一个会让初学者非常疑惑的点,因为基于大多数执行器接口实现类都采用异步执行方案的原因,并无法保证递交方法会在任务“消费”后返回,因此同步就显得毫无道理。那为什么还会有同步的说法呢?这是因为递交方法同步返回的Future(未来)拥有追踪任务执行状态的能力。因此即使任务被执行器异步执行,也可在需要时通过调用未来的get()方法达到同步“消费”(并获取执行结果/异常)的目的,其效果与任务在递交时即被同步“消费”是等价的,因此任务的“生产”与“消费”本质是一种间接/变相的同步。
同步“消费”的前提是确定具体需要被“消费”的任务。想要进行同步“消费”就必须先确定具体需要被“消费”的任务,这其中原因是因为我们需要调用该任务关联未来的get()方法。这并没有想象中简单,因为递交返回的未来未必是我们想要的目标未来,即递交的任务未必是我们想“消费”的任务。典型的例子是:如何获取一组递交任务中最早执行结束(完成/异常/取消)任务的结果呢?事实上开发者应该很清楚该问题在使用同步“消费”的情况下是无法/很难解决的,因为无法得知哪个同步未来的代表任务会最早执行结束(完成/异常/取消)。因此在包含上述举例在内的某些场景中我们也希望“消费”可以是异步的,即不通过调用同步未来get()方法的方式来“消费”任务,以达到对任务“消费”的顺序/时间/条件等多项维度进行自定义的目的。
CompletionService(完成服务)接口在定义上提供将任务的“生产”与“消费”从概念上分离的能力,而执行器完成服务类实现了这种能力,并支持按结束(完成/异常/取消)顺序对任务进行异步“消费”。完成服务接口在递交方法的基础上额外定义了移除方法对递交至完成服务的任务进行“消费”(并返回相关的未来),使得任务的“生产”与“消费”之间失去了交集而由原本的同步关系转变为了异步关系。因此作为完成服务接口的唯一实现类,执行器完成服务类天然具有分离任务“生产”与“消费”的能力。并且为了实现上述获取一组递交任务中最早执行结束(完成/异常/取消)任务结果的需求,执行器完成服务类会按结束(完成/异常/取消)的顺序对任务进行异步“消费”,即先被异步“消费”的任务一定比后被异步“消费”的任务先执行结束(完成/异常/取消)。该功能是通过代理将结束(完成/异常/取消)的任务依次加入阻塞队列中实现的,该功能会在下文详述。任务被异步“消费”后调用异步未来的get()方法,此时的get()方法已不再具有同步“消费”的意义,只单纯的被用来获取任务的执行结果。
三 创建
-
public ExecutorCompletionService(Executor executor) —— 创建指定执行器,且完成队列默认为链接阻塞队列的执行器完成服务。
-
public ExecutorCompletionService(Executor executor, BlockingQueue<Future> completionQueue) —— 创建指定执行器及完成队列的执行器完成服务。
四 方法
递交
-
public Future<V> submit(Callable task) —— 递交 —— 向当前执行器完成服务递交指定可回调/任务,并返回可追踪/获取指定可回调/任务执行状态/结果/异常的未来,但不推荐使用该未来等待指定可回调/任务执行结束。
-
public Future<V> submit(Runnable task, V result) —— 递交 —— 向当前执行器完成服务递交指定可运行/任务,并返回可追踪/获取指定可运行/任务执行状态/结果/异常的未来,但不推荐使用该未来等待指定可运行/任务执行结束。方法会同步传入用于承载指定可运行/任务执行结果/异常的变量,承载后指定可运行/任务执行结果/异常即可通过变量直接获取,也会向未来传递,因此该方法返回未来的get()方法可获取指定可运行/任务的执行结果/异常。
移除
-
public Future<V> poll() —— 轮询 —— 从当前执行器完成服务中移除并获取下个结束(完成/异常/取消)任务。该方法是移除方法“特殊值”形式的实现,当当前执行器完成服务存在结束(完成/异常/取消)任务时移除并返回下个结束(完成/异常/取消)任务;否则返回null。
-
public Future<V> take() throws InterruptedException —— 拿取 —— 从当前执行器完成服务中移除并获取下个结束(完成/异常/取消)任务。该方法是移除方法“阻塞”形式的实现,当当前执行器完成服务存在结束(完成/异常/取消)任务时移除并返回下个结束(完成/异常/取消)任务;否则等待至存在结束(完成/异常/取消)任务为止。
-
public Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException —— 轮询 —— 从当前执行器完成服务中移除并获取下个结束(完成/异常/取消)任务。该方法是移除方法“超时”形式的实现,当当前执行器完成服务存在结束(完成/异常/取消)任务时移除并返回下个结束(完成/异常/取消)任务;否则在指定等待时间内等待至存在结束(完成/异常/取消)任务为止,超出指定等待时间则返回null。
五 实现
完成队列
[completionQueue & 完成队列]是执行器完成服务类用于暂存未来的容器,结束(完成/异常/取消)任务的关联未来会被插入[完成队列]中,等待被移除/异步“消费”。执行器完成服务类设计[完成队列]有两个目的:一是将之作为异步“消费”的统一出口,即执行器完成服务类移除方法的本质其实就是从[完成队列]中移除未来并返回;二是基于队列的结构顺序实现按结束(完成/异常/取消)顺序异步“消费”任务的功能。因为除PriorityQueue(优先级队列)类、PriorityBlockingQueue(优先级阻塞队列)类及少数具备特殊功能的实现外,队列通常呈现FIFO(先入先出)的操作顺序。因此如果能够保证未来以代表任务的结束(完成/异常/取消)顺序插入[完成队列],就能够保证其按同样的顺序从[完成队列]中移除,这就达到了按结束(完成/异常/取消)顺序异步“消费”任务的目的。
执行/封装/钩子
注意:以下讲述内容需要对未来相关内容有一定的了解,否则可能难以理解关于封装的部分内容。
执行器完成服务类通过组合执行器提供对任务的间接执行能力。想要令未来以代表任务的结束(完成/异常/取消)顺序插入[完成队列],首先就需要解决任务的执行问题。执行器完成服务类并不应该具备任务的直接执行能力,即不依赖任何执行器独立执行任务。因为这会导致执行器异步“消费”时任务的执行脱离自身的控制,从而导致各种难以控制的后果发生,例如任务的执行时间错误、执行性能降低及执行结果不正确等。因此基于以上原因,执行器完成服务类选择通过组合[executor & 执行器]的方式提供对任务的间接执行能力。[执行器]会在创建执行器完成服务时具体指定,由此任务的执行还是会由外部调用执行器完成服务的执行器直接负责,这使得执行器完成服务类可与任何执行器配合使用。
执行器完成服务类通过重写钩子的方式令未来在代表任务结束(完成/异常/取消)时自动插入[完成队列]。所谓的钩子是指FutureTask(未来任务)类定义的done()方法,该方法默认为空实现,并且会在任务结束(完成/异常/取消)时调用,因此执行器完成服务类通过自实现私有内部类QueueingFuture(排队未来)的方式重写了该方法,用于在任务结束(完成/异常/取消)时将其关联未来加入到[完成队列]中。
执行器完成服务类会将递交的任务二度封装为排队未来。递交至执行器完成服务的任务需要先进行一度封装为未来用于在异步“消费”时返回。关于任务具体会被一度封装哪种未来与执行器完成服务的[执行器]有关,如果[执行器]为AbstractExecutorService(抽象执行器服务)类型,则其还会被转化保存在[aes & 抽象执行器服务]中用于将任务一度封装为[执行器]指定的RunnableFuture(可运行未来)接口实现类类型未来;否则固定将任务一度封装为未来任务。随后完成一度封装获得的未来会被继续二度封装为排队未来,并最终递交至[执行器]执行。排队未来类组合了[task & 任务]持有未来的引用,当任务执行结束(完成/异常/取消)并调用done()方法时,会将[任务]加入到[完成队列]中…相关源码如下:
private final BlockingQueue<Future<V>> completionQueue;
/**
* FutureTask extension to enqueue upon completion
* 未来任务衍生在完成后入队
*/
private class QueueingFuture extends FutureTask<Void> {
QueueingFuture(RunnableFuture<V> task) {
super(task, null);
this.task = task;
}
@Override
protected void done() {
// 将未来加入[完成队列]
completionQueue.add(task);
}
// 持有一度封装后未来的引用
private final Future<V> task;
}