1. 前言

当我们在 Java 中使用异步编程的时候,大部分时候,我们都会使用 Future,并且使用线程池的 submit 方法提交一个 Callable 对象。然后调用 Future 的 get 方法等待返回值。而 FutureTask 是 Future 的一个实现,也是我们今天的主角。

我们就从源码层面分析 FutureTask.

2. FutureTask 初体验

我们一般接触的都是 Future ,而不是 FutureTask , Future 是一个接口, FutureTask 是一个标准的实现。在我们向线程池提交任务的时候,线程池会创建一个 FutureTask 返回。

public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}

newTaskFor 方法就是创建一个了一个 FutureTask 返回。

protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}

而线程池就会执行 FutureTask 的 run 方法。

那么,我们看看 FutureTask 的 UML。

可以看出,FutureTask 实现了 Runnable,Future 。Runnable 就不必说了,一个 run 方法,那 Future 呢?

boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;

主要是这 5 个方法撑起了 Future,功能相对而言比较薄弱,毕竟这只是一个 Future ,而不是 Promise。

FutureTask 还有一个内部类,WaitNode ,结构如下:

static final class WaitNode {
volatile Thread thread;
volatile WaitNode next;
WaitNode() { thread = Thread.currentThread(); }
}

看起来是不是和 AQS 的节点似曾相识呢?

FutureTask 内部维护了一个栈结构,和 AQS 的队列有所区别。

实际上,在之前的版本中,FutureTask 确实直接使用的 AQS ,但是 Doug lea 又对该类进行了优化,优化的目的是 :

主要是为了避免有些用户在取消竞争期间保留中断状态。

而内部依然使用了一个 volatile 的 state 变量来控制状态,同时使用了一个栈结构来保存等待的线程。

至于原因,当然是 FutureTask 的 get 方法是支持并发的,多个线程可以获取到同一个 FutureTask 的同一个结果,而这些线程在 get 的阻塞过程中必然是要挂起自己等待的。

知道了 FutureTask 的结构。我们知道,线程池肯定会执行 FutureTask 的 run 方法,所以,我们到他的 run 方法看看。

同时,我们也要看看关键方法 —— get 方法。

3. FutureTask 的 get 方法

代码如下:

public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}

首先判断状态,然后挂起自己等待,最后,返回结果,代码很简单。

注意:FutureTask 中有 7 种状态:

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;

构造时,状态就是 NEW,当任务完成中,状态变成 COMPLETING。当任务彻底完成,状态变成 NORMAL。

我们重点看看 awaitDone 和 report 方法。

awaitDone 方法代码:

private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
for (;;) {
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
} int s = state;
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
else if (q == null)
q = new WaitNode();
else if (!queued)
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos);
}
else
LockSupport.park(this);
}
}

上面的方法相对于 JUC 其他的类,还是比较简单的。需要注意一个点:get 方法是可以并发访问的,当并发访问的时候,需要将这些线程保存在 FutureTask 内部的栈中。

简单说说方法步骤:

  1. 如果线程中断了,删除节点,并抛出异常。
  2. 如果字体大于 COMPLETING ,说明任务完成了,返回结果。
  3. 如果等于 COMPLETING,说明任务快要完成了,自旋一会。
  4. 如果 q 是 null,说明这是第一次进入,创建一个新的节点。保存当前线程引用。
  5. 如果还没有修改过 waiters 变量,就使用 CAS 修改当前 waiters 为当前节点,这里是一个栈的结构。
  6. 根据时间策略挂起当前线程。
  7. 当线程醒来后,继续上面的判断,正常情况下,返回数据。

再看看 report 方法:

private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}

也还是很简单的,拿到结果,判断状态,如果状态正常,就返回值,如果不正常,就抛出异常。

总结一下 get 方法:

FutureTask 通过挂起自己等待异步线程唤醒,然后拿去异步线程设置好的数据。

4. FutureTask 的 run 方法

上面总结说,FutureTask 通过挂起自己等待异步线程唤醒,然后拿去异步线程设置好的数据。

那么这个过程在哪里呢?答案就是在 run 方法里。我们知道,线程池在执行 FutureTask 的时候,肯定会执行他的 run 方法。所以,我们看看他的 run 方法:

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);
}
if (ran)
set(result);
}
} finally {
runner = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}

方法逻辑如下:

  1. 判断状态。
  2. 执行 callable 的 call 方法。
  3. 设置结果并唤醒等待的所有线程。

看看 set 方法是如何设置结果的:

protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}

先将状态变成 COMPLETING,然后设置结果,再然后设置状态为 NORMAL,最后执行 finishCompletion 方法唤醒等待线程。

finishCompletion 代码如下:

private void finishCompletion() {
// assert state > COMPLETING;
for (WaitNode q; (q = waiters) != null;) {
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
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; // unlink to help gc
q = next;
}
break;
}
} done(); callable = null; // to reduce footprint
}

该方法先将 waiters 修改成 null,然后遍历栈中所有节点,也就是所有等待的线程,依次唤醒他们。

最后执行 done 方法。这个方法是留个子类扩展的。FutureTask 中是个空方法。比如 Spring 的 ListenableFutureTask 就扩展了该方法。还有 JUC 里的 QueueingFuture 类也扩展了该方法。

如果异常了就将状态改为 EXCEPTIONAL。

如果用户执行了 cancel(true)方法。该方法 Java doc 如下:

试图取消对此任务的执行。如果任务已完成、或已取消,或者由于某些其他原因而无法取消,则此尝试将失败。当调用 cancel 时,如果调用成功,而此任务尚未启动,则此任务将永不运行。如果任务已经启动,则 mayInterruptIfRunning 参数确定是否应该以试图停止任务的方式来中断执行此任务的线程。

也就是说,这个 mayInterruptIfRunning 决定当任务已经在执行了,还要终止这个任务。如果 mayInterruptIfRunning 是 true ,就会先将状态改成 INTERRUPTING,然后调用线程的 interrupt 方法,最后,设置状态为 INTERRUPTED。

在 run 方法的 finally 块中,对 INTERRUPTING 有判断,也就是说,在 INTERRUPTING 和 INTERRUPTED 的这段时间,会执行 finally 块,那么这个时候,就需要自旋等待状态变成 INTERRUPTED。

具体代码如下:

private void handlePossibleCancellationInterrupt(int s) {
if (s == INTERRUPTING)
while (state == INTERRUPTING)
Thread.yield(); // wait out pending interrupt
}

5. 总结

关于 FutureTask 就介绍完了,该类最重要的就是 get 方法和 run 方法,run 方法负责执行 callable 的 call 方法并设置返回值到一个变量中, get 方法负责阻塞直到 run 方法执行完毕任务唤醒他,然后 get 方法回去结果。

同时,FutureTask 为了多线程可以并发调用 get 方法,使用了一个栈结构保存所有等待的线程。也就是说,所有的线程都等得到 get 方法的结果。

虽然 FutureTask 的设计很好,但我仍然觉得使用异步是更好的选择,效率更高。

并发编程—— FutureTask 源码分析的更多相关文章

  1. Java并发编程-ReentrantLock源码分析

    一.前言 在分析了 AbstractQueuedSynchronier 源码后,接着分析ReentrantLock源码,其实在 AbstractQueuedSynchronizer 的分析中,已经提到 ...

  2. Java并发编程 ReentrantLock 源码分析

    ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大. 这个类主要基于AQS(Abst ...

  3. 并发编程 —— Timer 源码分析

    前言 在平时的开发中,肯定需要使用定时任务,而 Java 1.3 版本提供了一个 java.util.Timer 定时任务类.今天一起来看看这个类. 1.API 介绍 Timer 相关的有 3 个类: ...

  4. Java并发编程-AbstractQueuedSynchronizer源码分析

    简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过 ...

  5. Java并发编程 LockSupport源码分析

    这个类比较简单,是一个静态类,不需要实例化直接使用,底层是通过java未开源的Unsafe直接调用底层操作系统来完成对线程的阻塞. package java.util.concurrent.locks ...

  6. java 并发编程——Thread 源码重新学习

    Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...

  7. FutureTask 源码分析

    FutureTask 源码分析,这个类的原理与我分析android当中的FutureTask类差不多[http://www.cnblogs.com/daxin/p/3802392.html] publ ...

  8. 并发工具CyclicBarrier源码分析及应用

      本文首发于微信公众号[猿灯塔],转载引用请说明出处 今天呢!灯塔君跟大家讲: 并发工具CyclicBarrier源码分析及应用 一.CyclicBarrier简介 1.简介 CyclicBarri ...

  9. Java异步编程——深入源码分析FutureTask

    Java的异步编程是一项非常常用的多线程技术. 之前通过源码详细分析了ThreadPoolExecutor<你真的懂ThreadPoolExecutor线程池技术吗?看了源码你会有全新的认识&g ...

随机推荐

  1. AngularJS 单元测试 Karma jasmine

    当AngularJS项目越来越大时候,需要进行单元测试,可以先开发功能再进行测试,也可以先进行测试. 一.karma  是一个基于Node.js(先要安装)的JavaScript测试执行过程管理工具( ...

  2. MCU_存储器

    MCU的存储器用途: RAM:数据存储器,和计算机的内存差不多,主要是用来存放程序运行产生的过程数据,掉电后会丢失数据,因此程序在上电后需要进行初始化.程序中的全局变量占据着RAM中的固定空间,局部变 ...

  3. 64位进程调用32位dll的解决方法

    64位进程调用32位dll的解决方法   最近做在Windows XP X64,VS2005环境下做32位程序编译为64位程序的工作,遇到了一些64位编程中可能遇到的问题:如内联汇编(解决方法改为C/ ...

  4. CentOS6.5分区与文件系统

    1 分区介绍 inux分区不同于windows,linux下硬盘设备名为(IDE硬盘为hdx(x为从a—d)因为IDE硬盘最多四个,SCSI,SATA,USB硬盘为sdx(x为a—z)),硬盘主分区最 ...

  5. iOS AppIcon尺寸

    如果提交的ipa包中,未包含必要的Icon就会收到类似的通知,为什么偏偏是Icon-76呢? 因为我们开发的游戏,默认是支持iphone以及ipad的,根据官方提供的参考 Icon-76.png是必须 ...

  6. MariaDB安装、初始化及常用操作

    1.Linux下安装与初始化 #安装 yum install mariadb-server #设置编码 vim /etc/my.cnf [mysqld]character-set-server=utf ...

  7. 在线团队协作工具+在线UML工具

    话不多说直接上https://worktile.com去看,顺便附上小众软件上面的介绍 默默增加worktile的外国原版https://trello.com/,worktile照着trello做的, ...

  8. VS2015+MySql+EF6采坑经验总结

    背景:VS2015+MySql+EF6(DB First) 采坑顺序:按照以前的记忆,操作依次如下: 1,安装 MySQL Connector/NET(不用想,装最新的,8.0.12) 2.安装 My ...

  9. Flask系列10-- Flask请求上下文源码分析

    总览 一.基础准备. 1. local类 对于一个类,实例化得到它的对象后,如果开启多个线程对它的属性进行操作,会发现数据时不安全的 import time from threading import ...

  10. iOS-项目开发1-图片浏览器

    FFBrowserImageViewController 自定义的图片浏览器:支持图片双击放大,单击取消,拖动取消. 重点: 1:在iOS11之后再布局是要将UIScrollViewContentIn ...