前言


 文章

一 概述


 简介

    FutureTask(未来任务)类是RunnableFuture(可运行任务)接口的实现类,故而其实例为Future(未来)的同时也是Runnable(可运行)。这意味着其不仅可以代表任务,本身也可以作为任务执行。这使得未来任务类具备了静态代理的特性,即支持在内部代理任务执行的上下文环境中自定义代理行为以实现设计需求,该知识点会在下文详述。

    未来任务类是线程安全的。未来任务类采用“无锁”线程安全机制,即使用CAS乐观锁来保证整体的线程安全。由于CAS乐观锁并不是真正意义上的锁,因此被称为“无锁”线程安全机制。

二 创建


  • public FutureTask(Callable callable) —— 创建指定可调用代理任务的未来任务。

  • public FutureTask(Runnable runnable, V result) —— 创建指定可运行代理任务的未来任务,并同步传入用于承载执行结果的变量。
        关于该构造方法,有个值得一说的知识点是可运行代理任务会在方法中被封装为可调用代理任务,因此从某种角度来说未来任务类实际上只支持执行可调用代理任务。将可运行代理任务封装为可调用打理任务是通过设计模式中的适配器模式来实现的,通俗的说,就是用某个可调用接口实现类组合可运行及用于承接执行结果的变量。当该实现类对象被执行,即call()方法被调用,其方法内部会调用可运行代理任务的run()方法来执行任务。

三 方法


  • boolean cancel(boolean mayInterruptIfRunning) —— 取消 —— 取消当前未来任务代表任务,取消成功则返回true;否则返回false。当代表任务结束(完成/异常/取消)时,由于已处于最终状态,代表任务将无法被取消,方法会返回false;而如果代表任务未结束(完成/异常/取消),则当『mayInterruptIfRunning @ 如果运行可能中断』为false时方法将阻止等待中的代表任务执行;而如果『如果运行可能中断』为true,则方法还将取消执行中的代表任务,即使代表任务可能无法响应取消。而只要取消操作(阻止/取消)成功执行,无论最终的结果如何,方法都将返回true。
        可以发现的是,cancel(boolean mayInterruptIfRunning)方法更多关注的是取消相关操作是否可以执行/是否成功执行(由于取消只能单次执行,因此并发可能导致失败),而并不关注取消相关操作执行后对代表任务的实际影响结果,特别是关于代表任务中断的部分。这实际上Java自身特性决定的,因为响应式中断的原因,代表任务的中断结果是无法预估的。这也就是说,只要取消相关操作可以成功执行,则无论代表任务最终的取消结果如何,其都会转变为取消状态。

  • public boolean isCancelled() —— 是否取消 —— 判断当前未来任务代表任务是否取消,是则返回true;否则返回false。

  • public boolean isDone() —— 是否结束 —— 判断当前未来任务代表任务是否结束(完成/异常/取消),是则返回true;否则返回false。

  • public V get() —— 获取 —— 获取当前未来任务代表任务的执行结果,在代表任务未结束(完成/异常/取消)之前,方法会无限等待。而根据代表任务最终状态的不同,方法会返回正常的执行结果,或抛出执行异常(代表任务自身执行时出现的异常)、取消异常(代表任务因cancel(boolean mayInterruptIfRunning)方法而被取消)及中断异常(采用非cancel(boolean mayInterruptIfRunning)方法的方式中断执行代表任务的线程)。

  • public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException —— 获取当前未来任务代表任务的执行结果,在代表任务未结束(完成/异常/取消)之前,方法会有限等待指定时间,超出指定等待时间将抛出超时异常。而根据代表任务最终状态的不同,方法会返回正常的执行结果,或抛出执行异常(代表任务自身执行时出现的异常)、取消异常(代表任务因cancel(boolean mayInterruptIfRunning)方法而被取消)及中断异常(采用非cancel(boolean mayInterruptIfRunning)方法的方式中断执行代表任务的线程)。

  • public void run() —— 运行 —— 执行当前未来任务代表任务,该方法通常由执行器线程调用。
        run()方法继承自可运行任务接口,被作为当前未来任务代表任务的执行入口。未来任务类在run()方法的实现中会调用其所代表的可调用任务,即可调用接口的call()方法,从而令之正式执行。并且为了实现设计上的需求,未来任务类会在call()方法执行的上下环境中嵌入代理行为,这就令该方法具备了静态代理的性质。事实上,未来接口的各个实现类大多都会在run()方法中嵌入代理行为以实现自身要求。该方法的大致结构如下图所示。

Java ~ Executor ~ FutureTask【总结】-LMLPHP

四 实现


 结构

Java ~ Executor ~ FutureTask【总结】-LMLPHP

 状态

    未来任务类自定义了近似于理论状态的实际状态来满足自身实现的需要。关于实际状态的定义及其与理论状态的对应关系具体如下:

    可以发现,实际状态对理论状态进行了一定的整合和细化,例如理论等待中/执行中被整合为NEW(0:新),理论取消被细化为了CANCELLED(4:已取消)/INTERRUPTED(6:已中断)。并在理论状态的基础上新增了COMPLETING(1:完成中)/INTERRUPTING(5:中断中)两个中间状态。自定义实际状态的目的是为了契合代表任务在未来任务中的实际运行流程,而根据代表任务运行时的各项可能,其实际状态变化存在以下几条链路:

 等待者链表的形成及清理

    等待者链表的基本单位是等待者,而等待者的本质是由于代理任务未结束(完成/异常/取消)而无法获取结果/异常的等待线程。这些线程被未来任务类内部实现的静态WaitNode(等待节点)类封装成为等待者/节点,并以链表的形式保存,形成了所谓的等待者链表。

    等待者链表是逻辑链表。所谓逻辑链表,是指其并不是类似于LinkedList(链接列表)的对象。未来任务[waiters @ 等待者链表]持有的只是单纯的等待者,该等待者作为等待者链表的头等待者被用于作为访问的入口。由于每个等待者都持有后继等待者的引用(尾等待者为null),因此只要持有了头等待者就相当于持有了整个等待者链表。等待者链表的形成流程大致如此:当获取线程调用未来任务的get系方法时,如果代理任务尚未结束(完成/异常/取消),即无法立即返回有效的结果/异常,则获取线程会进行无限/有限等待。对于未来任务来说,表示代理任务未结束(完成/异常/取消)的实际状态只有NEW(0:新)一个。当获取线程发现代理任务处于NEW(0:新)状态时会试图将自身封装为等待者,并以CAS操作头插至等待者链表中,成为新的头等待者。由于获取代表任务的获取线程可能不止一条,由此随着后续等待者的不断加入,等待者链表便随之形成。

    等待者成功加入等待者链表后会陷入无限/有限的等待状态,直至因为信号/中断/超时而唤醒。被唤醒的等待者会根据唤醒方式及代理任务当前实际状态执行不同的操作。如果是因为中断/超时而唤醒,则该等待者将不允许继续获取结果/异常。其会将自身设置为空等待者(即断开获取线程与等待者之间的引用,这么做的目的是为了辅助GC)后以遍历移除的方式从等待者链表中脱离以节省内存,并最后抛出中断/超时异常。而如果等待者是因为信号而唤醒,则方法会根据当前的实际状态来返回结果。即如果实际状态为NORMAL(2:正常)/EXCEPTIONAL(3:异常),则从[结果]中获取结果/抛出执行异常;而如果实际状态为CANCELLED(4:已取消)/INTERRUPTING(5:中断中)/INTERRUPTED(6:已中断),则获取线程会抛出取消异常。

    在上述等待者运行流程中,有个实际状态对应的操作十分特殊。获取线程在遭遇该状态时既无法做到立即获取结果/异常,却也不会将自身封装并加入等待者链表…这个状态就是COMPLETING(1:完成中)。按未来任务类的具体实现思想来说,COMPLETING(1:完成中)状态属于结束(完成/异常/取消)的范畴,但现实是由于该实际状态下结果/异常尚未被存入[结果]中,因此实际上获取线程并无法立即返回结果/异常。但由于保存结果/异常是非常快捷的赋值操作,因此该情况将获取线程封装并加入等待者链表是成本大于收益的行为,所以未来任务会令获取线程短暂放弃CPU资源,直到代表任务实际状态发生变化后再获取结果/异常。

    关于等待者链表,还有一个重要的知识点即是“清理”,等待者链表有“遍历移除”及“全移除”两种清理方式,分别发生于不同的等待者唤醒场景中。由于上文中我们已经提及了“遍历移除”,因此我们先从“遍历移除”开始讲解。遍历移除发生于等待者由于中断/超时而被唤醒的场景中,该场景下等待者不允许再继续获取结果/异常,因此其会将自身设置为空等待者并触发“遍历移除”。事实上,等待者再将自身置空后完全可以继续保留在等待者链表中,并最后由“全移除”清理。但为了节省内存,未来任务类还是选择主动的将之从等待者链表中移除…大佬写程序就是这么精益求精。

    “遍历移除”会从头遍历整个等待者链表,并沿途内部移除发现的每个空等待者,即重链接空等待者的前驱/后继等待者。比较特殊的操作是:而如果发现头等待者是空等待者,会通过CAS操作将其后继等待者设置为新头等待者。由于在此期间可能会出现新等待者头插等待者链表而造成的并发失败,因此需要重新从头遍历,以正常内部移除的方式移除该旧头等待者。可以发现“遍历移除”整体流程不但算不上复杂,甚至是十分简单的。但事实是再简单的流程在并发环境中都会变得莫名其妙…在上述流程中就可能存在“节点遗留”问题,即待移除等待者被遗留在等待者链表中…相关情况与前驱等待者的并发移除有关…具体场景如下图所示。

Java ~ Executor ~ FutureTask【总结】-LMLPHP

    当每个空等待者内部移除结束时,如果发现其前驱等待者为空等待者,则说明前驱等待者也执行了内部移除,则当前空等待者就存在被遗留的可能。为了杜绝这种可能,未来任务类选择直接采用重遍历的方式进行处理,即重新执行“遍历移除”,从而将“遗留”等待者的数量控制在一个相对较低的数量级。重遍历虽会对性能造成影响,但由于get系方法调用本身不属于高并发场景,因此等待者总数整体不会太多,故而相比使用悲观锁等方案而言性能影响反而要小得多。而对于同样存在“节点遗留”问题的LinkedTransferQueue(链接迁移队列)类来说直接重遍历便不是一个好方案,因为高并发环境会导致其“节点遗留”的概率更高而频繁触发重遍历,并且庞大的节点数量又使得遍历往往为长遍历,从而对整体性造成重大影响。因此链接迁移队列类引入了投票机制来进行缓解,只有当节点可能遗留的次数达到32次时才会执行重遍历。

    “全移除”相比遍历移除要简单的多,其会移除代表任务结束(完成/异常/取消)前加入等待者链表的所有等待者,并且只会发生一次。全移除会在等待者被代表任务执行线程唤醒时发生,又或者说是其会在代表任务结束(完成/异常/取消)时发生。由于代表任务已经结束(完成/异常/取消)的原因,执行线程会唤醒等待者链表中所有的等待者令其获取结果/异常,这就意味着所有的获取线程都将返回,而所有的等待者也都将变为空等待者,如此一来等待者链表也就没有存在的必要了,可以直接移除,即断开等待者链表(头等待者)与未来任务的链接。

    “全移除”并非直接断开未来任务与头等待者的链接就结束的,还需要遍历整个等待者外链(即已经与未来任务断开链接的等待者链表)以断开每个等待者之间的引用,这么做的目的是为了辅助GC,以避免跨代引用问题。虽说被称为“全移除”(名字我自己瞎取的,包括上面的“遍历移除”),但实际由于并发的原因,并不一定能移除所有的等待者。事实上,在等待者链表与未来任务断开链接成为等待者外链后,可能还会有新的等待者加入等待者链表生成等待者内链,具体情况如下图所示。

Java ~ Executor ~ FutureTask【总结】-LMLPHP

    虽然现实中存在等待者内链产生的问题,但首先由于这部分等待者的数量是相对较少的,因此对内存的浪费会很小。其次由于代表任务已经结束(完成/异常/取消),因此加入等待者链表后获取线程也不会进入等待状态而是会立即获取结果/异常并返回,故而也无需担心这部分获取线程会永久等待而无法被唤醒。获取线程返回后,留下的空等待者会随同未来任务一起被GC回收。

 重复执行

    对于代表任务,通常没有重复执行的说法,但事实是未来任务类确实提供了可供代表任务重复执行的方法,具体如下:

  • protected boolean runAndReset() —— 运行并重置 —— 执行当前未来任务代表任务,但不保存执行结果,并返回是否可再次执行。是则返回true;否则返回false。

    runAndReset()方法被修饰了protected关键字,表示其专为子类准备,无法直接调用。runAndReset()方法在逻辑上与run()方法高度一致,唯一的区别在于当代理任务执行完成时并不会在未来任务中保存结果,也就是说其并不存在“正常”链路。那也就是说,只要代表任务在执行时不抛出异常,并且没有调用cancel(boolean mayInterruptIfRunning)方法/run()方法,那代表任务就可以通过不断调用runAndReset()方法来重复执行。

Java ~ Executor ~ FutureTask【总结】-LMLPHP

    注意:由于runAndReset()方法不会在未来任务中保存执行结果,因此在只调用runAndReset()方法的情况下get()方法会永久阻塞。当然,如果代理任务执行异常或被cancel(boolean mayInterruptIfRunning)方法取消则依然可以获取到执行/取消异常。

 取消中断和常规中断

    对于cancel(boolean mayInterruptIfRunning)方法想必都已不再陌生,其作用是取消当前代表任务。如果『如果运行可能中断』为false,则其只会阻止等待中的代表任务执行;而如果为true,则方法还会尝试中断执行中的代表任务,即使代表任务可能无法响应中断。而在未来任务类的实现中,无论取消相关操作对代表任务的影响如何,其都不会再保存代表任务的执行结果/异常。

    关于cancel(boolean mayInterruptIfRunning)方法还有一个值得提及的知识点:即当『如果运行可能中断』为true时其中断的作用范围。在具体讲解之前,我们需先了解何为中断的作用范围。众所周知,中断任务实质是指中断任务的执行线程。又因为Java只存在响应式中断,因此所谓中断任务的执行线程又是指将线程的中断状态设置为true。由于中断状态是线程的固有属性,故而中断的作用范围即是指在线程的整个生命周期/运行流程中该中断操作导致的true状态的覆盖范围。

Java ~ Executor ~ FutureTask【总结】-LMLPHP

    在明白何为中断的作用范围后,我们来继续讲解取消中断的作用范围。在定义上,cancel(boolean mayInterruptIfRunning)方法是专为取消代表任务而设计的,故而取消中断的作用范围理论上最多只能覆盖到代表任务的结束位置,否则后续流程将可能响应该中断,造成整体流程的错误执行。而无论是否存在该情况,取消中断的作用范围超出都是不规范的行为,因此在代表任务结束(完成/异常/取消)后应该固定存在兜底响应操作,以确保线程执行代表任务完毕进入下个阶段时中断状态必定为false。事实上未来任务类确实这么做了,被作为run()/runAndReset()方法内具体的代理行为之一,具体如下图所示。

Java ~ Executor ~ FutureTask【总结】-LMLPHP

    比较搞笑的是,虽然未来任务类确实实现了兜底响应操作,但又被其自身所取消了(即源码被注销),原因是因为程序允许使用中断代理任务执行线程的方式对流程进行干预,例如将执行线程的中断状态作为某操作是否执行的标记位,以及ThreadPoolExecutor(线程池执行器)类在终止时强行中断所有的执行线程等。这种情况下执行线程的中断状态不允许被还原,但由于程序无法区分中断状态是否由cancel(boolean mayInterruptIfRunning)方法造成,因此注销了本该执行的Thread.interrupted()方法操作。因此,“兜底响应中断”除了确保代理任务在中断链路中一定会在run()/runAndReset()方法内转变为INTERRUPTED(6:已中断)状态之外没有任何其它作用,具体源码及注释如下:

/**
 * Ensures that any interrupt from a possible cancel(true) is only delivered to a task while in run or runAndReset.
 * 确保来自可能的 cancel(true)方法的任意中断只能在运行run()方法或runAndReset()方法时才能传递给任务。
 *
 * @Description: 名称:处理可能的取消中断
 * @Description: --------------------------------------------------------
 * @Description: 作用:消除cancel()方法对任务执行线程造成的中断状态,避免对执行器的后续流程造成影响。cancel()方法的作用是取消
 * @Description: 任务,因此其造成的线程中断理论上只能作用于任务结束执行前的流程。而如果cancel()方法造成的线程中断状态在任务结
 * @Description: 束之后还继续保留,则可能会对后续的流程造成影响,因此需要将之还原。但问题在于程序允许调用者使用取消/中断的方
 * @Description: 式对任务的执行流程进行干预,例如将执行线程的中断状态作为某段特殊操作的标记位使用等,这种情况下的线程中断状
 * @Description: 态是不允许还原的。但由于无法区别执行线程的中断状态是cancel()方法造成还是其它操作造成的,因此注销了本该执行的
 * @Description: Thread.interrupted()方法操作。因此,当前方法除了确保任务在被中断的情况下一定会在run方法中转变为INTERRUPTED
 * @Description: (6:已中断)状态以外没有任何其它作用。
 * @Description: --------------------------------------------------------
 * @Description: 逻辑:方法会判断当前任务是否会被执行中断,即判断状态是否为INTERRUPTING(5:中断中)。如果需要,则不断的放
 * @Description: 弃CPU资源,直至状态被修改为INTERRUPTED(6:已中断)。
 */
private void handlePossibleCancellationInterrupt(int s) {
    // It is possible for our interrupter to stall before getting a chance to interrupt us.  Let's spin-wait patiently.
    // 我们的中断者可能有机会在打断我们之前暂停。让我们耐心等待。

    //     如果任务状态为INTERRUPTING(5:中断中),则不断的放弃CPU资源,直至状态被修改为INTERRUPTED(6:已中断)。这么做
    // 这么做的目的是确保cancel()方法造成的线程中断状态只限制在run()/runAndReset()方法中。只有当状态为INTERRUPTED(6:已中断)
    // 时,才确定cancel()方法已经执行了中断。
    //     cancel()方法的作用是取消任务,因此其造成的线程中断理论上只能作用于任务结束执行前的流程。而如果cancel()方法造成的线程
    // 中断状态在任务结束之后还继续保留,则可能会对后续的流程造成影响,因此需要将之还原。
    if (s == INTERRUPTING)
        while (state == INTERRUPTING)
            // wait out pending interrupt
            // 等到待定中断结束
            Thread.yield();

    // assert state == INTERRUPTED;

    // We want to clear any interrupt we may have received from cancel(true).  However, it is permissible to use interrupts as an
    // independent mechanism for a task to communicate with its caller, and there is no way to clear only the cancellation interrupt.
    // 我们需要清除任何我们可能从cancel()方法中接收的中断。但是,允许使用中断作为任务与调用者通信的独立机制,并且没有办法只
    // 清除取消中断。(这句话是指允许调用者使用取消/中断的方式对任务的执行流程进行干预,例如将执行线程的中断状态作为某段特
    // 殊操作的标记位使用等,这种情况下的线程中断状态是不允许还原的。但由于无法区别执行线程的中断状态是cancel()方法造成还是
    // 其它操作造成的,因此注销了本该执行的Thread.interrupted()方法操作)

    // Thread.interrupted();
}

 自定义

    如果仔细看过上文中run()/runAndReset()方法的详细结构图,会发现在具体的代理行为中存在“自定义”。自定义代理行为发生在唤醒所有等待者之后,此时代理任务及相关代理行为基本已经执行完毕,出于可能存在开发者希望在代理任务结束(完成/异常/错误)时执行某些自定义操作的考虑,未来任务类提供了钩子方法done()来满足该需求。开发者可以自实现未来任务类的子类并重写done()方法,以实现在代理任务结束(完成/异常/错误)时执行某些自定义操作…done()方法的源码如下。

/**
 * Protected method invoked when this task transitions to state {@code isDone} (whether normally or via cancellation). The default
 * implementation does nothing.  Subclasses may override this method to invoke completion callbacks or perform bookkeeping. Note
 * that you can query status inside the implementation of this method to determine whether this task has been cancelled.
 * 当这个任务转变状态为结束(即非等待中状态)(不论是正常或通过取消结束)时调用的受保护方法。默认实现什么也不做。子类可
 * 能为了调用回调或执行统计而重写这个方法(执行一些自定义操作)。注意:你可以在这个方法的实现中查询状态以查明这个任务是
 * 否已经取消(该方法的作用就是在取消操作(状态修改 + 等待者换新)后执行一些自定义的操作)。
 *
 * @Description: 名称:结束
 * @Description: --------------------------------------------------------
 * @Description: 作用:用于子类实现在任务结束(完成/异常/取消)并唤醒等待线程后可以执行一些自定义操作。
 * @Description: --------------------------------------------------------
 * @Description: 逻辑:~
 */
protected void done() {
}
06-30 05:00