前言
FutureTask是多线程编程中经常用到的一个类,在线程池中,它常常以 executor.submit() 方法调用中的形式隐式存在。它返回了一个Future对象,从而实现了多线程编程中获取异步结果的一种途径。
FutureTask是一个封装了任务的类,可以理解成是Runnable/Callable的包装类。原生的Runnable/Callable都是同步执行,FutureTask像是一个单独的容器,通过持有任务的成员变量,并获取执行该任务的线程,让任务在这FutureTask中运行,然后将运行结果存放在容器中。调用者可以在不同的阶段获取这个结果。
帮忙办件事
故事: 同学A接到了快递电话,要求马上到校门口取快递,否则要等到下午再过来。正要出门赶往校门口取快递,肚子疼了起来。于是同学A交给了同学B一个装有监控的盒子,让同学B去校门口将东西装在盒子里拿回来,自己则跑去厕所蹲起了茅坑,玩起了手机,并监控起了盒子情况。
这是一个很常见的异步执行过程,普通的Runnable和Callable也能实现。但是与它们不同的是,FutureTask会返回一个Future对象,就像同学A的监控器,可以随时知道快递的当前情况。而Runnable和Callable则没有。看下例:
以Runnable为例:
Runnable r = () -> System.out.println("取快递");
new Thread(r).start();
FutureTask:
FutureTask t = new FutureTask(() -> System.out.println("取快递"), true);
new Thread(t).start();
都是异步编程,FutureTask有什么过人之处呢?
在Runnable例子中,引用 r 无法获取任务当前执行结果。但是在FutureTask中,引用 t 可以获取任务当前执行结果。
它是怎么做到的呢?
FutureTask
看一下继承结构,FutureTask实现了RunnableFuture,RunnableFuture又继承了Runnable和Future接口。所以FutureTask既是一个Runnable,也是一个Future。
它的核心设计思想是,将真正的Runnable/Callable和执行它的Thread封装到FutureTask里面,并将它们的执行结果储存起来。
成员变量
根据这个思路看一下它的成员变量:
private Callable<V> callable; // 真正的任务
private Object outcome; // 任务执行的最终结果
private volatile Thread runner; // 执行任务的线程
private volatile WaitNode waiters; // 简单的线程链表
在FutureTask的设计思想中,执行结果是当任务完成时返回,所以持有的成员变量是Callable,因为Runnable执行完成后不会返回结果。而Runnable在FutureTask中会被转换为Callable。
回到故事中,当同学A看盒子时,可能会存在几种结果?
- 同学B可能还没出发,盒子里还是空的
- 同学B正在路上,盒子里还是空的也可能有快递了。
- 同学B已经取回来了,盒子里有快递了。
根据可能的结果,FutureTask设计了对应的字段,以满足不同阶段的状态。
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
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
由于对于一个FutureTask对象而言,任务状态可能会被多个线程同时改变,所以 state 是一个线程不安全的变量,使用了volatile修饰,满足线程并发和线程可见性。
sun.misc.Unsafe
从FutureTask整个代码中可以看见,state基本上全是多线程取值操作,没有多线程对state的赋值操作。多线程的赋值操作全是采用的CAS的底层sun.misc.Unsafe类来实现。
在本类的开头有这样一句注释:
* Style note: As usual, we bypass overhead of using
* AtomicXFieldUpdaters and instead directly use Unsafe intrinsics.
大意就是java本来提供了采用CAS实现的Atomic类群,但Atomic类群其实底层是用的sun.misc.Unsafe,所以本类绕过了Atomic类直接使用sun.misc.Unsafe,提高效率。
CAS是原意是比较并交换,所以它是两个操作的原子性操作集合。volatile修饰的变量显然不能满足在这种操作下满足原子性。
所以在多线程环境下对volatile修饰的三个变量:
private volatile int state;
private volatile Thread runner;
private volatile WaitNode waiters;
线程安全的取值:依赖volatile可见性;
线程安全的赋值:依赖sun.misc.Unsafe实现的CAS机制
这里注意:
下面两个为什么用volatile修饰,并且WaitNode是什么?
private volatile Thread runner;
private volatile WaitNode waiters;
这是因为还是先前的故事,可以发展成更复杂的情况。
同学B就是执行FutureTask的线程,FutureTask就是这个盒子, 同学A就是持有FutureTask引用的线程。开头就描述了FutureTask是一个对Runnable/Callable的包装任务,它只是一个任务管理者,真正执行的任务是FutureTask持有的Runnable/Callable。
看一下FutureTask的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(); // 这里实际是调用的Callable.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);
}
}
这里面,FutureTask.run 实际上是调用的c.call()执行任务。
为了防止被并发执行。FutureTask对Thread runner做了并发控制,试想如果每个拿到这个FutureTask的线程都去执行这个任务,就会出现并发问题,导致结果与预期不一致。
if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
return;
// 通过CAS选择一个执行者,并通过volatile告诉其他人已经有人正在执行了。
// 如果要执行也必须等当前执行者完成。因为完成后runner会退出。
finally {
runner = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
WaitNode是什么?
在搞清这个问题之前,回到故事原型中。同学A可能将监视器复制一个给同学E,然后同学E可能也复制一个给同学Z…,这样导致会有很多的同学都可以监视当前任务的状态。而反过来,对于FutureTask而言,意味着会有很多的查看者,也就是线程。
为了管理这些的线程,FutureTask持有了一个WaitNode,它是一个内部类:
static final class WaitNode {
volatile Thread thread;
volatile WaitNode next;
WaitNode() { thread = Thread.currentThread(); }
}
从它的结构可以看出,这是一个简单的单向链表。这些闯入到FutureTask里的线程会根据先后顺序,被管理在WaitNode链表上。为了让可能并发的线程强制形成先后顺序,WaitNode也使用了volatile修饰,
private volatile WaitNode waiters;
并且使用CAS机制处理并发。代码里可见一斑(截取代码片段):
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null))
queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q);
...
既然可能会有多个线程调用,那么调用的入口在哪里?
那就是get()系列方法.
get() 和 get(long timeout, TimeUnit unit)
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
public V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (unit == null)
throw new NullPointerException();
int s = state;
if (s <= COMPLETING &&
(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
throw new TimeoutException();
return report(s);
}
在get()系列方法中,一个没有超时限制,一个有超时限制。它们内部都是通过调用的awaitDone(boolean timed, long nanos)方法来处理调用线程,
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);
}
}
在代码中,整个执行在一个for无限循环中,如果当前线程没有加入到WaitNode队列,将会执行CAS加入,
queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q);
如果通过超时限制的get()进入方法,则当时间超过限制,会跳出无限循环,否则会执行LockSupport.parkNanos(this, nanos);
而通过没有超时限制的get()进入,会调用LockSupport.park(this);。
好像探索越来越深了,什么是LockSupport.parkNanos(this, nanos) 和 LockSupport.park(this)?
LockSupport.parkNanos() 和 LockSupport.park()
LockSupport是一个对native层 sun.misc.Unsafe方法的包装,提供了LockSupport.parkNanos() 和 LockSupport.park()还有其他几个方法,简单理解,LockSupport.parkNanos()类似与Object.wait(time), 而LockSupport.park()类似于Object.wait(0), 但它们与wait方法有些差异,这里不做展开介绍。
在这个方法里可以理解成, LockSupport.parkNanos()是限时阻塞,而LockSupport.park() 是无限阻塞。
所以,当一个调用futureTask的线程调用了get()方法,如果futureTask结果没有出来,当前线程会被阻塞在这里。被阻塞的线程都在WaitNode链表中限时或者无限沉睡。直到FutureTask异步结果出来调用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
}
在finishCompletion()方法中,会通过LockSupport.unpark()唤醒WaitNode链表中的每一个线程,并且释放内存。
这时这些线程才得以从get()方法的无限循环中跳出去。
最后介绍一个方法,cancel(boolean mayInterruptIfRunning)
cancel(boolean mayInterruptIfRunning)
这个方法提供了参数 : 中断正在执行的任务。在方法的实现中,它其实就是调用了Thread.interrupt()方法,给当前线程加上了中断标志而已。 但是mayInterruptIfRunning含义中理解,它有可能无法中断,因为在下达中断命令之前,futureTask任务可能在任何一个可能的状态中。具体是怎样的呢?
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();
} finally { // final state
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}
}
} finally {
finishCompletion();
}
return true;
}
从代码中的条件入口判断理解:
if (!(state == NEW && UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
return false;
在FutureTask中有一个执行线程,假设它是A线程,它负责执行具体的任务。而发出中断指令的是外部线程,可能是同学E,可能是同学Z…, 因此对于futureTask状态的操作存在竞争,所以在这个条件判断中的含义就是:
NEW 代表当前任务执行结束前。结束可能由四个状态,这在开头提到过。
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
如果当前任务结束之前(可能正在运行,也可能还未开始),并且发出中断执行的线程成功将任务状态切换了,则cancel()将会跳过if判断执行下面的代码,否则无法中断。
如果能够中断:
则当mayInterruptIfRunning为ture,将会调用执行线程的Thread.interrupt(),给执行线程加上了中断标志。
mayInterruptIfRunning为false,则是将当前任务状态直接由NEW转到CANCELLED. 如果线程在调用之前是在执行,则线程在调用之后可能依然在执行,那么任务执行完成后,还能获取到正常的结果吗?
看这个细节:
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);
}
}
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
执行线程执行任务时,线程被阻塞在result = c.call();执行完成后,将会调用 set(result);
而由于执行期间任务状态已经被改为了CANCELLED,这时,set方法中CAS操作将会执行失败,结果无法返回给outcome。
所以这种情况下,是拿不到结果的。
最后
futureTask是多线程编程非常基础和重要的一个类,了解它的机制,相互协作及运行原理,有利于未来多线程编程各种业务下的实战。
如有不正,请多指点。