FutureTask原理介绍——cancel功能

137 阅读7分钟

前言

本来打算下班回来就开写的,打开电脑,面对无尽的源码,审视自己疲惫的躯壳,发现无论如何下不了手。于是翻开了《闯入不适区》,刺激一下精神,接着去洗了个澡。回来已是23:40,我咬了咬牙,还是输出一点吧,能写多少是多少。

介绍cancel功能

FutureTask实现了cancel功能,该功能就是等待线程可以对正在执行或即将执行的任务发送cancel命令,本质上就是对执行该任务的线程发送一个interrupt标识。此处翻译一下源码的方法。

/**
 * Attempts to cancel execution of this task.  This attempt will
 * fail if the task has already completed, has already been cancelled,
 * or could not be cancelled for some other reason. If successful,
 * and this task has not started when {@code cancel} is called,
 * this task should never run.  If the task has already started,
 * then the {@code mayInterruptIfRunning} parameter determines
 * whether the thread executing this task should be interrupted in
 * an attempt to stop the task.
 *
 * <p>After this method returns, subsequent calls to {@link #isDone} will
 * always return {@code true}.  Subsequent calls to {@link #isCancelled}
 * will always return {@code true} if this method returned {@code true}.
 *
 * @param mayInterruptIfRunning {@code true} if the thread executing this
 * task should be interrupted; otherwise, in-progress tasks are allowed
 * to complete
 * @return {@code false} if the task could not be cancelled,
 * typically because it has already completed normally;
 * {@code true} otherwise
 */
boolean cancel(boolean mayInterruptIfRunning);

这里分享一个阅读源码的心得:看接口注释,包括接口、方法注释。对于接口方法来说,首先,它通常对其实现功能、注意事项、用法、参数和返回值含义等进行了定义,可以说是一种约定。尽管其实现类有各种不同的实现,但必须遵守其接口的约定。只有首先了解约定,才能更好的去理解每种不同的实现,做到纲举目张。一个约定,有各种千奇百怪的实现,这也是一件十分有趣、十分令人好奇的事情。

话不多说,对上面的英文进行一波翻译(上一次翻译不是为了毕业就是在考研):

尝试取消该任务的执行。如果这个任务已经执行完成,或者已经被取消,又或者由于一些其他原因不能被取消,本次取消的动作将失败(不会带来任何变化或影响)。如果取消成功,并且该任务在该方法被调用之前还没有开始,这个任务将永不会被执行。如果一个任务已经开始,那么根据mayInterruptIfRunning这个参数来决定是否要中断(interrupt)一个正在执行任务的线程来停止任务。

在这个方法返回之后,后续对isDone()方法的调用都将会true。如果该方法返回true,那么后续对isCancelled方法的调用也都将返回true

@param mayInterruptIfRunnningtrue,代表正在执行该任务的线程将被中断(interrupt);否则,正在执行的任务就继续执行直到完成。 @returnfalse,代表该任务无法被取消,通常这是因为任务已经正常执行完毕;其他情况则返回true


0913 continue

阅读cancel源码

解析任务状态

增加cancel功能之后,原任务状态只有完成/未完成两个状态,使用的是boolean类型,当前需要新增一个取消状态,因此需要修改该字段改为int state。同时,我们需要列举所有可能的状态:

  • NEW:代表初始状态
  • FINISHIED:代表任务已完成
  • CANCELED:代表人物已取消
  • INTERRUPTED:已中断。该字段与cancel方法的入参对应,当任务正在执行时,接收到取消的命令,则在适当时候停下任务,进入中断状态。
Class MyFutureTask {
    private volatile int state;
    private static final int NEW = 0;
    private static final int FINISHED = 1;
    private static final int CANCELLED = 2;
    private static final int INTERRUPTED = 3;
}

而实际源码还添加了COMPELTING和INTERRUPTING两个状态。

阅读cancel方法

下面我们先来解析cancel方法。根据接口描述,cancel失败的情况有三种:

  1. 任务已完成;
  2. 任务已被取消(不能重复取消);
  3. 由于其他原因无法被取消(主要是多线程并发取消时,只能有一个取消成功);

cancel成功的情况有两种:

  1. mayInterruptIfRunningtrue,若线程正在执行,将发送中断信号,最终state为INTERRUPTED
  2. 否则,若线程正在执行,将允许执行完毕,最终state为CANCELLED

一旦任务被成功取消,等待队列中的线程都将会唤醒,且调用get()最终会抛出异常CancellationException

public boolean cancel(boolean mayInterruptIfRunning) {
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {    // in case call to interrupt throws exception
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner; // 这里再一次用到了快照模式
                if (t != null)
                    t.interrupt(); // 此处可能runner已经为null,代表任务已经执行完毕
            } finally { // final state
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        finishCompletion();// 唤醒所有的等待线程
    }
    return true;
}

public void run() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex); // 如果此时已经被cancel,则该方法无影响
            }
            if (ran)
                set(result); // 同上
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

根据源码说明几种并发状态

  1. 中断正在执行的线程,线程响应了中断并清除;
执行线程等待线程
起始是NEW状态
1 c.call() // 任务执行中
2. NEW -〉 INTERRUPTTING
3. t.interrupt() // 中断执行线程
4. setException(ex) //执行到可中断点抛出中断异常
6. INTERRUPTTING -> INTERRUPTED
7. 执行finaly
8. 执行finally
  1. 中断正在执行的线程,但执行线程没有中断点,导致执行线程在执行期间内未响应,最终也未清除中断标记。
执行线程等待线程
起始是NEW状态
1 c.call() // 任务执行中
2. NEW -〉 INTERRUPTTING
3. t.interrupt() // 中断执行线程
4. set(result) // 无可中断点
6. INTERRUPTTING -> INTERRUPTED
7. handlePossibleCancellationInterrupt
8. 执行finally

根据handlePossibleCancellationInterrupt的源码可以看到,作者将清除中断标记的代码注释掉了,这是为了让该中断保留在执行线程中,让后续的代码得知该线程被中断过,也就是该中断标记虽然在线程执行当前任务的过程中没有被响应,但后续到达可中断点时依然会被响应

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.
    if (s == INTERRUPTING)
        while (state == INTERRUPTING)
            Thread.yield(); // wait out pending interrupt

    // 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.
    //
    // Thread.interrupted(); // 这里被注释掉了。。。
}
  1. 想要中断正在执行的线程,但执行线程已经与任务脱钩了,导致没有发送中断信号。
执行线程等待线程
起始是NEW状态
1 c.call() // 任务执行中
2. NEW -〉 INTERRUPTTING
3. set(result) // 无效果
4. runner = null
5. handlePossibleCancellationInterrupt
6. t = runner
7. if(t!=null) // 为空,不会interrupt了
8. INTERRUPTTING -> INTERRUPTED
9. finally

这种情况下,等待线程以为自己发送成功了中断信号,但实际上因为线程调度的问题,执行线程并没有收到中断信号,既没有清除,也没有传递可言。

那么问题就是,第2点和第3点,线程调度的时机不同,代码的执行结果不一致,是否有问题??

结语

FutureTask的基本原理介绍就差不多了,核心的地方以及大体实现思路均已说明,其他的边边角角自行看代码即可掌握。本次最大的感受是,阅读源码不容易,读懂后输出更难,通俗易懂的输出难上加难!