FutureTask源码阅读

本文详细分析了Java的FutureTask类,探讨了其状态机设计,成员变量(如状态、Callable对象、WorkerThread和WaiterThread)以及run和get方法的工作原理。作者解释了任务的不同状态和状态转换,以及如何通过cancel和interrupt控制任务执行。同时提到了weakCompareAndSet与compareAndSet的区别。
摘要由CSDN通过智能技术生成


本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习

本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,否则你会不知道我在说什么

简介

FutureTask是Future的一个实现,提供了可取消的异步任务执行。FutureTask具体实现了RunnableTask这个接口:

public interface RunnableFuture<V> extends Runnable, Future<V> {
  void run();
}

作为Future,为什么实现了Runnable而不是Callable呢,因为实现Runnable接口可以方便地将这个任务交给Executor执行,然后任务内部封装了一个Callable对象,并且在run方法中最终会调用Callable,并将结果保存在内部,当调用get的时候获取结果。

例子

Callable<String> task = ...; // 实际执行的任务
FutureTask<String> future = new FutureTask<>(task);
executor.execute(future); // 执行任务
future.get(); // 获取结果

代码分析

成员变量

FutureTask的核心是状态机,即内部将Waiter Thread抽象出各种状态,底层通过Thread和LockSupport检测线程状态并改变线程的实际状态(阻塞、中断等),而状态机的状态转换是通过CAS进行状态转换的。先来看看FutureTask为任务定义的几种状态

public class FutureTask<V> implements RunnableFuture<V> {
  // 表示任务的状态。状态初始化为NEW,所有可能的状态转换如下:
  // NEW -> COMPLETING -> NORMAL
  // NEW -> COMPLETING -> EXCEPTIONAL
  // NEW -> CANCELLED
  // NEW -> INTERRUPTING -> INTERRUPTED
  private volatile int state;
  
  // 刚创建FutureTask到任务执行中这段时间内都处于NEW状态
  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;

一定要注意,这里的状态是任务的状态,而不是worker thread的状态,也不是waiter thread的状态

分析代码逻辑前,先明确一下这些状态的具体含义。

首先两个过渡状态COMPLETING、INTERRUPTING怎么理解呢?拿COMPLETING举例子:

// 任务运行结束后保存返回结果
protected void set(V v) {
  // 设置为COMPLETING,防止其它线程设置为其他状态
  if (STATE.compareAndSet(this, NEW, COMPLETING)) {
    // 保存结果
    outcome = v;
    // 设置为NORMAL
    STATE.setRelease(this, NORMAL);
    finishCompletion();
  }
}

这下知道了,“保存结果”属于临界区操作,并且从逻辑上来说只能在保存结果后,才能去设NORMAL,因此就只能先设置一个过渡状态COMPLETING(完成中),防止其他线程同时进入临界区。

至此,我们只需要关注NEW、NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTED这几个状态,因为COMPLETING只是用来保护NORMAL/EXCEPTIONAL的,同理INTERRUPTING保护INTERRUPTED,没有实际的作用。

其次,NORMAL和EXCEPTIONAL也可以归为一类,两者都表示任务结束,前者对应任务正常返回,后者对应任务抛出异常。

综上,我们最终只需要关注NEW、NORMAL/EXCEPTIONAL(视为一种状态)、CANCELLED、INTERRUPTED四种状态,其中NORMAL/EXCEPTIONAL、CANCELLED、INTERRUPTED这几个状态都属于最终状态(terminal),也就是状态一旦变成他们其中的任何一种,都不会再变成其他状态。

最后简单看看其他成员变量

// 实际需要运行的任务,通过构造函数传入
private Callable<V> callable;
// 保存执行结果或执行抛出的异常
private Object outcome;
// Worker Thread
private volatile Thread runner;
// 保存所有的Waiter Thread,相当于阻塞队列(实际上是Treiber stack)
private volatile WaitNode waiters;

最后一个waiters实际上是阻塞栈(称为Treiber stack,一种lock-free的栈),即FILO。之所以不用队列,我认为是因为这些阻塞者之间并没有竞争锁的关系,一旦任务完成或取消,他们一下子全部被释放,因此没必要将他们公平地释放,不差那几纳秒。其次,相比队列得要两个指针分别指向头和尾,栈只需要一个栈顶指针就能方便地操作,主打一个简单。waiters并不是重点,知道是用来干什么的就可以。

方法

构造函数就不看了,比较简单。

根据先有鸡后有蛋的原则,先分析任务的执行(run),再分析结果的获取(get)。先看看被Woker Thread真正用于执行任务的run方法:

public void run() {
  // 如果state不是NEW了,或者已经存在runner,说明任务正在/已经 被 完成/取消,直接返回
  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) {
        // 任务自身抛出异常,setException设置结果为异常
        result = null;
        ran = false;
        setException(ex);
      }
      // 任务正常返回,保存结果
      if (ran)
        set(result);
    }
  } finally {
    // 至此任务已经完成,做收尾工作
    runner = null;
    int s = state;
    if (s >= INTERRUPTING)
      handlePossibleCancellationInterrupt(s);
  }
}

流程主要就是,状态非NEW或者runner不为空的话就不用运行任务,因为它可能已经在运行或者已经运行结束。然后运行任务后通过set或者setException设置结果。最后还有一个非常细节的handlePossibleCancellationInterrupt方法,不过得放在文末说,因为还涉及到其他的方法,目前就先当他没有任何用途。

接下来再看get方法:

public V get() throws InterruptedException, ExecutionException {
  int s = state;
  // 如果任务还没有变成终态,则进行阻塞等待
  if (s <= COMPLETING)
    s = awaitDone(false, 0L);
  // 阻塞结束,返回结果
  return report(s);
}

get比较简单,看一下awaitDone是怎么阻塞等待的:

private int awaitDone(boolean timed, long nanos) throws InterruptedException {
  long startTime = 0L;    // Special value 0L means not yet parked
  FutureTask.WaitNode q = null;
  boolean queued = false;
  for ( ; ; ) {
    int s = state;
    // 分支1:如果任务已经是终态,则直接返回
    if (s > COMPLETING) {
      if (q != null)
        q.thread = null;
      return s;
    }
    // 分支2:如果任务正处于过渡态,则自旋等待任务进入终态
    else if (s == COMPLETING)
      Thread.yield();
    // 分支3:如果线程被中断,那么将其从等待栈删除,并抛中断异常
    else if (Thread.interrupted()) {
      removeWaiter(q);
      throw new InterruptedException();
    }
    // 分支4:如果没有被阻塞的其他线程
    else if (q == null) {
      // 如果不需要等待,直接返回
      if (timed && nanos <= 0L)
        return s;
      // 创建节点准备入栈
      q = new FutureTask.WaitNode();
    }
    // 分支5:如果还没入栈,将其入栈
    else if (!queued)
      queued = WAITERS.weakCompareAndSet(this, q.next = waiters, q);
    // 分支6:如果为有限等待,计算等待时长并且parkNanos
    else if (timed) {
      final long parkNanos;
      // 计算应该等待的时长
      if (startTime == 0L) {
        startTime = System.nanoTime();
        if (startTime == 0L)
          startTime = 1L;
        parkNanos = nanos;
      } else {
        long elapsed = System.nanoTime() - startTime;
        // 如果已经超时,则交给上层负责抛出超时异常
        if (elapsed >= nanos) {
          removeWaiter(q);
          return state;
        }
        parkNanos = nanos - elapsed;
      }
      // 上面nanoTime函数可能耗时长,此时任务状态可能已经变成了终态,进行二次检查
      if (state < COMPLETING)
        LockSupport.parkNanos(this, parkNanos);
    }
    // 分支7:如果为无限等待,直接park
    else
      LockSupport.park(this);
  }
}

awaitDone使用了一个for( ; ; )无限循环,里面有多个分支处理不同的情况,这是想干嘛。看过Doug Lea大神写过的其他一些代码就知道,比如AQS中的acuqire、enqueue函数等,for( ; ; ) { if else }这种写法其实也构成了一个FSM(有限状态机),只不过没有像FutureTask那样显式地抽象成几个状态常量,而是根据当前某些变量的值选择进入对应的分支,然后进入下一个状态或终态…注意要与FutureTask任务的状态区分开来,这里只是将awaitDone这块代码逻辑用状态机的方式理解而已。

7个分支分别对应7个状态,有的状态是过渡态(分支1,3,分支4中不需要等待的情况),有的是终态,具体的逻辑写在注释里。

看下removeWaiter如何实现:

private void removeWaiter(FutureTask.WaitNode node) {
  if (node != null) {
    node.thread = null;
    retry:
    for (;;) {
      // pred为q的前驱,q为被移除节点,s为q的后继
      for (FutureTask.WaitNode pred = null, q = waiters, s; q != null; q = s) {
        s = q.next;
        if (q.thread != null)
          pred = q;
        // 让pred.next指向s,即移除q
        else if (pred != null) {
          pred.next = s;
          if (pred.thread == null)
            continue retry;
        }
        else if (!WAITERS.compareAndSet(this, q, s))
          continue retry;
      }
      break;
    }
  }
}

removeWaiter其实不只移除node,还移除其他所有无效节点(thread==null的节点,因为不再与线程绑定,节点已经没用了)。注意最外层循环,这个循环是用来当发生race的时候重新遍历栈的,尽最大的努力去移除所有的无效节点。之所以是“尽最大努力”,是因为它只检测本次遍历的前驱节点或栈顶,如果是中间某个已经遍历过的节点成为了无效节点,只能下一次removeWaiter的时候才能检测出来。

removeWaiter主要还是针对单个节点的移除,如果任务已经结束,还需要一次性移除所有节点,也就是finishCompletion函数完成的功能:

private void finishCompletion() {
  for (FutureTask.WaitNode q; (q = waiters) != null;) {
		// 直接通过null out the waiters删除栈,保证只有一个线程在释放栈上的节点
    if (WAITERS.weakCompareAndSet(this, q, null)) {
      for (;;) {
        Thread t = q.thread;
        // 唤醒线程
        if (t != null) {
          q.thread = null;
          LockSupport.unpark(t);
        }
        // 遍历下一个节点
        FutureTask.WaitNode next = q.next;
        if (next == null)
          break;
        q.next = null;
        q = next;
      }
      break;
    }
  }

  done();

  callable = null;        // to reduce footprint
}

最外层的循环是为了避免有节点waiter意外入队,进行二次检查。

ok,至此,任务的运行与任务结束一套流程都讲完了,除此之外,用户还有两种方式控制任务的结束。第一种是通过cancel结束任务,第二种是直接通过对正阻塞在get的waiter thread调用interrupt,这样awaitDone中的park会马上返回,然后检查interrupted发现被中断了会抛中断异常。下面来看一下cancel的方式,cancel是对任务进行中断,但是要提前说明的是,cancel可以将任务标记为已取消,但不一定能实际结束正在运行的任务

public boolean cancel(boolean mayInterruptIfRunning) {
  if (!(state == NEW && STATE.compareAndSet
                               (this, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
    return false;
  try {
    // 如果通过中断来取消任务
    if (mayInterruptIfRunning) {
      try {
        Thread t = runner;
        if (t != null)
          t.interrupt();
      } finally {
        STATE.setRelease(this, INTERRUPTED);
      }
    }
  } finally {
    // unpark所有waiters
    finishCompletion();
  }
  return true;
}

cancel比较简单,只是需要注意,这里mayInterruptIfRunning是根据任务来传true或false的,比如正在执行的任务是可以响应中断的,那么可以传个true,让其响应中断从而顺利结束任务。而如果任务响应不了中断,那么你传true或false都没用,因为interrupt使得任务响应中断。通过cancel可以唤醒waiters,并且之后的get都会被告知任务已取消,但如果任务本身不响应中断,一直在跑的话,是没办法控制的,这个已经超出了FutureTask的管理范围,需要用户自行设计worker thread与外界的交互,比如让worker thread可以响应中断,举个可以响应中断的例子:

ExecutorService executorService = Executors.newCachedThreadPool();
Callable<String> task = () -> {
  int i = 0;
  while (true) {
    System.out.println("hello" + i++);
    Thread.sleep(1000); // Thread.sleep能响应中断
  }
};
var future = new FutureTask<>(task);
executorService.execute(future);

Thread.sleep(100); // 让worker thread运行起来
future.cancel(true); // 传true表示尝试调用interrupt中断worker thread

try {
  System.out.println(future.get()); // 这里会抛CancellationException
} finally {
  executorService.shutdown();
  System.out.println("should shutdown");
}

这个例子中,如果task内没有像Thread.sleep这样能响应中断的方法,或者cancel传了个false,那么就会不断打印hello1…hello2…

最后再回头看看之前介绍run方法时遗留的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();
}

看起来他好像啥也没做,但其实是为了解决一个隐蔽的bug:在cancel(true)的内部,会尝试对runner.interrupt:

Thread t = runner;
if (t != null) // 第一行
  t.interrupt(); // 第二行

如果在第一行执行完后,发生线程调度,调度到工作线程,然后run方法刚好跑完,然后该线程在线程池的安排下去执行了其他的任务,此时线程调度回执行cancel(true)的线程,它继续运行第二行,诶,它本来要中断的是之前的那个任务,但interrupt是针对线程而不是针对任务进行中断的,此时就错误地中断了第二个任务,造成bug。

因此handlePossibleCancellationInterrupt的目的是让中断的发生在run还没退出前发生,这样就不会错误地中断该工作线程执行的下一个任务。

补充

注意到waiters的设置有时候是通过WAITERS.weakCompareAndSet,那么weakCompareAndSet与compareAndSet有什么区别呢?可以参考这篇回答,结合这个回答,单纯从使用的角度上来说,如果cas的重试的成本不高,那么可以考虑使用weakCompareAndSet,而如果java代码没有循环cas,比如只是简单的if(cas),或者重试成本比较高,比如removeWaiter中CAS失败后的重试意味着要重新遍历栈,那么可以考虑使用compareAndSet。总而言之是权衡了软件或硬件来实现功能的成本。

参考链接

「简书」【细谈Java并发】谈谈FutureTask

  • 19
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值