前言
文章
- 相关系列:《Java ~ Executor【目录】》(持续更新)
- 相关系列:《Java ~ Executor ~ AbstractExecutorService【源码】》(学习过程/多有漏误/仅作参考/不再更新)
- 相关系列:《Java ~ Executor ~ AbstractExecutorService【总结】》(学习总结/最新最准/持续更新)
- 相关系列:《Java ~ Executor ~ AbstractExecutorService【问题】》(学习解答/持续更新)
- 涉及内容:《Java ~ Executor【总结】》
- 涉及内容:《Java ~ Executor ~ ExecutorService【总结】》
- 涉及内容:《Java ~ Executor ~ ExecutorCompletionService【总结】》
- 涉及内容:《Java ~ Executor ~ Future【总结】》
- 涉及内容:《Java ~ Executor ~ FutureTask【总结】》
- 涉及内容:《Java ~ Executor ~ Callable【总结】》
- 涉及内容:《Java ~ Thread ~ Runnable【总结】》
一 概述
简介
AbstractExecutorService(抽象执行器服务)抽象类是ExecutorService(执行器服务)接口的抽象实现类,其作用是对执行器服务接口的部分方法定义进行流程规划。所谓流程规划,可以理解为不完整实现。即整体运行流程虽然已经编码,但其中调用的部分方法却没有实现或没有正常实现。这部分方法可能是抽象方法,也可能是直接抛出不支持操作异常的破坏性实现,子类有权利/义务具体实现这些调用方法以使得整个运行流程变得完整可用。这种在父类中规划具体流程而将各个功能的实现细节交由子类负责的编码模式被称为“模板模式”,是常见设计模式中的一种。在Java的各个框架中基本以Abstract为名称开头的类都采用了该模式实现,例如Collection(集)框架的AbstractCollection(抽象集)抽象类、AbstractList(抽象列表)抽象类及AbstractSet(抽象集合)抽象类等。抽象模式的使用极大降低/减少了子类实现的难度/代码量,因为子类无需再去重复的关注/设计/实现那些通用的传递/连接/转换性质的中间代码,而可以集中精力专注在那些具体功能的实现上。不过虽说好处如此,但由于“模板模式”在整体流程的规划上需要兼容所有的子类,因此通常难以保证全局性能的优异性,即无法保证所有子类在各自实现理念上都能达到相对较高的性能。故而即使有时父类已经基于“模板模式”对某方法进行了默认的流程规划,但部分子类出于性能上的考量还是会选择重写该方法。
抽象执行器服务抽象类规划了执行器服务接口定义的所有与任务递交相关方法的运行流程,该知识点的内容会在下文详述。
模板模式结构图
二 流程规划
递交
-
submit(Runnable task)
-
submit(Runnable task, T result)
-
submit(Callable<T> task)
递交系方法的作用是向当前执行器递交可运行/可调用/任务,并返回可追踪/获取可运行/可调用/任务执行状态/结果的Future(未来)。
抽象执行器服务抽象类将递交系方法的流程规划划分为“可运行/可调用/任务封装”及“可运行/可调用/任务执行”两个功能。所谓“可运行/可调用/任务封装”功能即是将传入的可运行/可调用/任务封装为未来以用于后续执行及方法返回,该功能由抽象执行器服务抽象类自定义实现的newTaskFor(新任务)系方法完成。新任务系方法的实现非常简单,其会直接将可运行/可调用/任务作为构造方法的参数来创建默认的FutureTask(未来任务)。具体源码如下:
- protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) —— 新任务 —— 为指定的可运行/任务和用于承接执行结果的变量创建可运行未来。
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
- protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) —— 新任务 —— 为指定的可调用/任务创建可运行未来 。
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
未来任务类是抽象执行器服务抽象类默认规定的未来接口实现类,即如果没有重写规则,可运行/可调用/任务会被默认封装为未来任务。但现实是抽象执行器服务抽象类子类有时需要与未来接口实现类进行专项绑定,即针对不同实现思想的抽象执行器服务抽象类子类,可能需要为之定制专项未来接口实现类以封装可运行/可调用/任务。因此为了支持自由化选择,抽象执行器服务抽象类将新任务系方法设计为protected访问权限,从而令抽象执行器服务抽象类子类可以重写该系方法以实现对未来接口实现类的定制。这种提供了功能默认实现但又允许其自定义重写的做法也是“模板模式”继不实现(抽象方法)及破坏性实现之外的另一种使用方式。而更多时候,对于默认实现功能的方法通常都会被修饰final关键字,即不允许重写。
“可运行/可调用/任务执行”功能由execute(Runnable command)方法负责执行。抽象执行器服务抽象类并没有对execute(Runnable command)方法提供默认实现,即该方法为无实现的抽象方法,抽象执行器服务抽象类子类需要重写该方法以令递交系方法可用,并可通过其对可运行/可调用/任务的执行方式进行自定义实现。
调用任意
-
invokeAny(Collection<? extends Callable<T>> tasks)
-
invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
调用任意系方法的作用是向当前执行器递交指定可调用/任务集,并返回其中一个已完成可调用/任务的执行结果。该方法会有限/无限地等待递交的可调用/任务集中的一个执行完成(异常/取消不计算在内),无论最终是否超时,其余未结束(等待中/执行中)的可调用/任务都将被取消。而如果在指定等待时间内所有可调用/任务都执行失败(异常/取消)则会抛出执行异常;而超时则会抛出超时异常。由于Java是响应式中断,因此取消的具体表现为等待中的可调用/任务不再执行,而执行中可调用/任务的执行线程会被中断,但具体能否取消成功则由任务能否响应中断及执行时机决定。
抽象执行器服务抽象类对调用任意系方法的流程规划完成度非常之高,已经接近完全实现的地步。execute(Runnable command)方法是调用任意系方法仅有还需要子类实现的功能,其流程规划大致如下:
- 创建未来集,用于容纳由指定可调用/任务集中的可调用/任务封装而来的未来;
- 循环指定可调用/任务集,依次将可调用/任务递交至ExecutorCompletionService(执行器完成服务)并将返回的未来加入未来集中。每次递交之前方法都会尝试从执行器完成服务中获取结束(完成/异常/取消)的未来,如果成功获取,则会暂时中止递交并判断未来是否完成。是则准备返回结果;否则说明该结束(完成/异常/取消)未来为异常/取消,需要继续向执行器完成服务递交可调用/任务;
- 所有可调用/任务递交后,方法会有限/无限地等待执行器完成服务直至获取到完成未来的结果或因超时而抛出超时异常为止。此外还有一种特殊情况,即所有未来都异常/取消。如此情况下会将最后未来的异常(执行/取消/运行时异常)记录下来并封装为执行时异常抛出。如果不存在异常记录,说明是程序没有覆盖到的其它异常,此时会抛出一个默认的执行异常。
- 无论方法最终是成功获取到完成未来的结果、还是抛出超时/执行异常,返回前都要先对未来集中的所有未来调用cancel(boolean mayInterruptIfRunning)方法,目的是取消未来集中尚未结束(完成/异常/取消)的未来。但这么做会对未来集中结束(完成/异常/取消)的未来执行无意义操作,因为结束(完成/异常/取消)的未来是无法被取消的。
执行调用任意系方法前要判断执行器中是否存在未执行任务。由于方法会等待至返回其中一个已完成可调用/任务的执行结果,因此如果执行器中存在大量的任务未执行,则可能导致调用任意系方法长时间的等待。并且与之前的任务并发执行也会影响调用任意系方法的执行效率。因此在执行调用任意系方法前要判断执行器中是否存在未执行任务。
调用所有
-
invokeAll(Collection<? extends Callable<T>> tasks)
-
invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
调用所有系方法的作用是向当前执行器递交指定可调用/任务集,并返回追踪/获取可调用/任务执行状态/结果/异常的未来集。方法会有限/无限地等待递交的可调用/任务集执行结束(完成/异常/取消),如果超时则会取消其中未结束(等待中/执行中)的可调用/任务,因此返回未来集中所有未来的isDone()方法调用结果都为true。由于Java是响应式中断,因此取消的具体表现为等待中的可调用/任务不再执行,而执行中可调用/任务的执行线程会被中断,但具体能否取消成功则由任务能否响应中断及执行时机决定。
与调用任意系方法相同,调用所有系方法同样也只依赖子类实现execute(Runnable command)方法。虽说没有采用共同的底层方法,但两者的逻辑大致相同,其流程规划如下:
- 创建未来集,用于容纳由指定可调用/任务集中所有可调用/任务封装而来的未来;
- 循环指定可调用/任务集,依次将可调用/任务封装为未来并加入未来集中,随后执行未来。invokeAll(Collection<? extends Callable> tasks)方法会在一次循环同步完成封装与执行两步操作;而invokeAll(Collection<? extends Callable> tasks, long timeout, TimeUnit unit)方法则会分两次循环分别进行,即一次循环封装未来,二次循环执行未来。后者分为两次进行的原因是因为可调用/任务的执行逻辑由子类负责实现,同步执行会难以保证在指定等待时间内将所有可调用/任务封装为未来并加入未来集,导致未来集不完整,所以才会设计专属封装的循环来避免。在流程规划上,由于单纯的封装是非常简单快捷的操作,因此程序默认忽略了该过程的时间消耗;
- 所有未来执行后,方法会在限/无限的时间里循环未来集,并依次调用get()/get(long timeout, TimeUnit unit)方法等待未来执行结束(完成/异常/取消)。由于get()/get(long timeout, TimeUnit unit)方法是等待方法,因此只有当前未来结束(完成/异常/取消)后才可以对下个未来进行调用;
- 如果成功等待到所有的未来执行结束(完成/异常/取消),则方法会直接返回未来集;而如果get(long timeout, TimeUnit unit)方法抛出超时异常导致不再等待后续未来,则方法在返回未来集前要先对未来集中的所有未来调用cancel(boolean mayInterruptIfRunning)方法,目的是取消未来集中尚未结束(完成/异常/取消)的未来。但这么做会对未来集中结束(完成/异常/取消)的未来执行无意义操作,因为结束(完成/异常/取消)的未来是无法被取消的。
执行调用所有系方法前要判断执行器中是否存在未执行任务。由于方法会等待至递交的可调用/任务集执行结束(完成/异常/取消),因此如果执行器中存在大量的任务未执行,则可能导致调用所有系方法长时间的等待。并且与之前的任务并发执行也会影响调用所有系方法的执行效率。因此在执行调用所有系方法前要判断执行器中是否存在未执行任务。