前言

当咱们运用线程池submit一个使命后,会回来一个Future,而在Future接口中存在一个cancel办法,来帮助咱们撤销掉使命。

可是cancel办法有一个boolean类型的入参,比较迷惑,之前也了解过该入参true 和 false的差异,但过一段时间之后就又忘了,遂写了本文进行记录,趁便了解下源码

/**
 * 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);

上面是cancel办法的接口界说,当然英文看着麻烦,咱直接翻译成看得懂的~

  • cancel办法,会尝试撤销使命的履行,但假如使命现已完成、现已撤销或其他原因无法撤销,则尝试撤销使命失利。

  • 假如撤销成功,并且在撤销时

    • 该使命还未履行,那么这个使命永远不会履行。
    • 假如该使命现已启动,那么会根据cancelboolean入参来决定是否中止履行此使命的线程来中止使命。

经过注释咱们大致能了解到cancel的一个效果,可是还不够详尽,接下来咱们经过源码解读具体的带咱们了解一下~


FutureTask使命状况认知

首先,咱们先了解下FutureTask中对使命状况的界说

在运用线程池submit后,实际上是回来的一个FutureTask,而FutureTask中对于使命界说了以下状况,并且在注释中,也界说了状况的流转进程~

/**
 * Possible state transitions:
 * NEW -> COMPLETING -> NORMAL
 * NEW -> COMPLETING -> EXCEPTIONAL
 * NEW -> CANCELLED
 * NEW -> INTERRUPTING -> INTERRUPTED
 */
private volatile int state;
private static final int NEW     = 0;
private static final int COMPLETING  = 1;
private static final int NORMAL    = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED  = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;

可是经过对上面状况界说的了解,咱们能够发现,在FutureTask中并没有一个标明使命处于履行中的一个状况!

直接看FutureTaskrun办法源码

public void run() {
  if (state != NEW ||
    !RUNNER.compareAndSet(this, 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);
       }
      if (ran)
        // 正常履行结束
        set(result);
     }
   } finally {
   //... 省掉
   }
}
​
protected void setException(Throwable t) {
 if (STATE.compareAndSet(this, NEW, COMPLETING)) {
  outcome = t;
  STATE.setRelease(this, EXCEPTIONAL); // final state
  finishCompletion();
  }
}
​
protected void set(V v) {
 if (STATE.compareAndSet(this, NEW, COMPLETING)) {
  outcome = v;
  STATE.setRelease(this, NORMAL); // final state
  finishCompletion();
  }
}

经过上面源码,咱们也能了解到

  • 当使命正常履行结束时,使命状况流转: NEW -> COMPLETING -> NORMAL
  • 使命履行反常时,使命状况流转: NEW -> COMPLETING -> EXCEPTIONAL

所以,当使命刚创建,或许是使命在履行进程中,使命的状况都是NEW


cancel源码剖析

此刻再来剖析cancel源码

public boolean cancel(boolean mayInterruptIfRunning) {
  // NEW为新建或许运转态
  // 1. 此刻使命现已不是NEW,阐明要么是完成要么是反常,撤销不了,所以回来false
  // 2. 此刻使命仍是NEW,假如咱们传入true,则CAS标记使命为INTERRUPTING,否则是CANCELLED
  // 避免并发撤销使命,CAS只会有一个线程成功,其余线程失利
  if (!(state == NEW && STATE.compareAndSet
      (this, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
    return false;
  try {  
    // 传入true,则打断该使命的履行线程
    if (mayInterruptIfRunning) {
      try {
        Thread t = runner;
        if (t != null)
          t.interrupt();
       } finally {
        // 比较使命状况为INTERRUPTED
        STATE.setRelease(this, INTERRUPTED);
       }
     }
   } finally {
    finishCompletion();
   }
  return true;
}

经过对FutureTask使命状况的认知,再结合对cancel源码的剖析

咱们能够总结出以下定论

  • 当使命现已完成或许反常时,无法撤销使命

  • 使命处于新建或许运转状况时

    • cancel办法入参传入true

      • 将使命状况NEW -> INTERRUPTING -> INTERRUPTED,并打断履行该使命的线程
    • cancel办法入参传入false

      • 将使命状况NEW -> CANCELLED

但有个问题,传入false仅仅将状况从NEW变成CANCELLED嘛,这好像没啥用啊?

当然不是,此刻咱们需要再回头看看FutureTaskrun办法

public void run() {
  if (state != NEW ||
    !RUNNER.compareAndSet(this, 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);
       }
      if (ran)
        // 正常履行结束
        set(result);
     }
   } finally {
   //... 省掉
   }
}

run办法开头咱们能够看到,假如使命的状况不是NEW,那么会直接return,不履行使命

那此刻再想想传入false将使命状况从NEW -> CANCELLED,是不是当使命还没有开端履行时,咱们cancel(false)就能够撤销掉未履行的使命了~


总结

经过上面的源码解读,咱们大致能了解了cancel的机制,可是咱们仍是完善的总结一下

  • 使命假如不是NEW状况是不会履行的

  • cancel撤销使命会改变使命的状况

    • 假如传入true, 则将使命状况NEW -> INTERRUPTING -> INTERRUPTED,并打断履行该使命的线程
    • 假如传入false,将使命状况NEW -> CANCELLED

传入false只能撤销还未履行的使命

传入true,能撤销未履行的使命,能打断正在履行的使命


扩展知识点

cancel源码中,咱们能够看到finally中会去调用finishCompletion

那么,finishCompletion是干啥的呢?

private void finishCompletion() {
  // assert state > COMPLETING;
  for (WaitNode q; (q = waiters) != null;) {
    // 原子性将WAITERS设置为null
    if (WAITERS.weakCompareAndSet(this, q, null)) {
     
      // 遍历WAITERS,将堵塞的线程都唤醒
      for (;;) {
        Thread t = q.thread;
        if (t != null) {
          q.thread = null;
          LockSupport.unpark(t);
         }
        WaitNode next = q.next;
        if (next == null)
          break;
        q.next = null;
        q = next;
       }
      break;
     }
   }
​
  // 扩展办法,交给自己完成
  done();
​
  callable = null;
}

咱们能够想想,当咱们submit一个使命时,一般情况下都会需要去获取他的回来值,会调用get办法进行堵塞获取

FutureTask中,会保护一条链表,该链表记录了等候获取该使命回来值被堵塞的线程

在调用get办法时,会将组装waiters链表

揭秘Future cancel迷惑性boolean入参~

所以,当咱们撤销一个使命时,是不是也应该去将堵塞等候获取该使命的一切线程进行唤醒finishCompletion办法就是做这个工作的~


结尾

我是 皮皮虾 ,会在今后的日子里跟咱们一起学习,一起进步!

觉得文章不错的话,能够在 关注我,或许是我的公众号——JavaCodes