FutureTask是在JDK1.5被引入的,又是并发大神Doug Lea的杰作。
它代表的是一个异步计算任务,它将在未来的某个时刻完成计算并获得一个计算结果。当线程调用get()去获取结果时,如果计算完毕,则立马返回,否则线程会被阻塞,直到计算完成。
应用场景例如:你需要导出一份Excel,但不是马上要下载,此时你可以创建一个FutureTask实例,并提交到线程池中运行,当用户想要下载时,通过FutureTask获取到Excel的生成路径去找到文件并响应给客户端。
UML
FutureTask实现了
Runnable接口,代表它可以交给线程去执行,同时又实现了Future接口,代表它是一个异步任务,在未来的某个时间可以获得一个结果,而且任务是可以取消的。
源码导读
FutureTask没有依赖AQS,相对比较完整和独立。
它有一个内部类WaitNode,代表阻塞线程的节点,这些节点构成了一个简单的单向链表。
当线程调用get()获取结果时,如果计算还没有结束,FutureTask则会调用awaitDone()将当前线程入对并Park。
属性
callable变量用来保存带有返回值的计算任务。
outcome用来保存计算结果,如果计算出异常了,则保存的是异常信息。
runner用来记录执行计算的线程。
waiters是一个单向链表,记录的是等待计算结果的阻塞线程。
// 带返回值的任务,运行结束后会被清空
private Callable<V> callable;
// 计算的结果,如果计算过程中异常了,则保存异常信息。
private Object outcome;
// 执行计算任务的线程
private volatile Thread runner;
// 等待队列,单向链表
private volatile WaitNode waiters;
state用来表示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;//线程已经被中断
构造函数
FutureTask提供了两个构造函数:
- 传入
Callable,任务开始调用它的call()获得计算结果。 - 传入
Runnable和结果result,run()执行结束后,返回result,这种属于事先就定义好结果了。
/*
给定一个Callable,将在run()中执行它,并保留结果
*/
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
/*
给定一个Runnable和结果result。
将在run()中执行它,并在执行结束后返回result。
*/
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
核心方法
get
get()可以获取计算结果,如果任务没有计算完毕,线程会阻塞。
FutureTask提供了两种get()方法,都是响应中断的:
- get() 获取结果,否则无期限等待,直到计算完成,或线程中断。
- get(long timeout, TimeUnit unit) 支持超时的获取结果,直到计算完成、超时、或线程中断。
逻辑是一样的,判断状态,如果是NEW或COMPLETING则需要调用awaitDone()让线程等待一会儿,否则说明任务计算完成或者失败了,不需要阻塞,返回结果即可。
/*
获取计算结果,这个过程是阻塞的,需要等待计算完成。
*/
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
/*
两种状态下需要等待:
1.NEW:任务新建好,还在计算过程中。
2.COMPLETING:计算好了,但是还没保存计算结果,这时不会入队Park,
而是Thread.yield(),暂时让出CPU的执行权,等待保存计算结果。
*/
s = awaitDone(false, 0L);
// 计算完成,或异常,取消了都直接返回,不等待。
return report(s);
}
/*
支持超时的获取计算结果,LockSupport.parkNanos()会指定线程挂起的时间。
*/
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);
}
先看awaitDone()等待的逻辑:
- 判断线程是否中断,是则抛异常,并移除节点,因为get()是响应中断的。
- 判断
state是否大于COMPLETING,是则说明任务计算完成或失败,已经不需要等待了。 - 判断
state是否等于COMPLETING,是则说明计算已经完成,只是还没有保存结果而已,马上就快好了,此时线程调用Thread.yield()暂时让出CPU的执行权,过会儿再来看结果。 - 如果还没计算完毕,则创建
WaitNode入队并Park。
/*
等待计算完成:
何时终止?
1.计算完成了。
2.超时了。
3.被中断了。
*/
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) {
/*
状态大于COMPLETING就不用阻塞了,可以返回了:
1.计算完成了。
2.任务被取消了。
3.计算过程抛异常了。
4.计算任务被中断了。
*/
if (q != null)
q.thread = null;
return s;
}
else if (s == COMPLETING)
/*
计算完成还不能返回,需要等待计算结果保存到outcome,
并将state设置为:NORMAL。
此时可以让出CPU的执行权。
*/
Thread.yield();
else if (q == null)
// 创建节点,准备入队
q = new WaitNode();
else if (!queued)
/*
CAS+自旋的方式,头插法入队。
为什么采用头插法?
1.没有用单独的属性来记录尾巴,如果采用尾插法,每次都需要从链头查找到链尾,效率很低。
2.并发的时候,可能存在节点全部唤醒完毕,但是新建节点才刚刚入队,无法被唤醒的问题。
*/
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
// 成功入队,挂起当前线程,等待run()计算完成后唤醒
LockSupport.park(this);
}
}
等待超时、线程被中断都需要移除节点:
/*
当任务被中断,或超时,需要移除节点。
并不会只移除传进来的node节点,而是对整个链表检查一遍,thread=null的节点都要移除。
*/
private void removeWaiter(WaitNode node) {
if (node != null) {
node.thread = null;
retry:
for (;;) { // restart on removeWaiter race
for (WaitNode pred = null, q = waiters, s; q != null; q = s) {
s = q.next;
if (q.thread != null)
pred = q;
else if (pred != null) {
pred.next = s;
/*
移除的过程中,前面又有新的节点需要移除,则跳出循环重新检查一遍。
后面的节点无所谓,因为本身就是从前向后扫描的。
*/
if (pred.thread == null)
continue retry;
}
else if (!UNSAFE.compareAndSwapObject(this, waitersOffset,
q, s))
continue retry;
}
break;
}
}
}
如果计算完成了,则通过report()返回计算结果,它有三种情况:
- 计算正常完成,返回结果。
- 任务被取消了,抛
CancellationException。 - 计算任务本身异常了,抛
ExecutionException。
// 报告任务计算的结果,或者抛出异常。
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);
}
run
run()方法不应该显式的调用,而是交给线程池去调用。
线程池调用run(),其实就是执行Callable,获取返回值,并唤醒等待线程。
// 执行任务
public void run() {
/*
开始计算有两个前提:
1.state为NEW新建状态。
2.任务的执行线程为null,通过CAS的方式修改为当前线程。
确保多线程并发执行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) {
// 计算过程发生异常,结果为null,ran为false
result = null;
ran = false;
setException(ex);//保存异常信息,并通知节点
}
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)
// 任务被中断了,自旋调用Thread.yield(),暂时让出CPU的执行权,等待state设为INTERRUPTED。
handlePossibleCancellationInterrupt(s);
}
}
计算有两种结果:异常和正常。
计算正常:set(V v)
// 保存计算结果
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 保存计算结果
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
// 唤醒阻塞节点
finishCompletion();
}
}
计算异常:setException(Throwable t)
/*
任务计算过程中异常了,保存异常信息,调用get()时返回给客户端。
*/
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 保存异常信息
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
// 计算异常了,也要唤醒阻塞的节点。
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
}
FutureTask在任务结束时,调用了done(),这是一个钩子函数,默认什么都不做,是交给子类去扩展的。
cancel
FutureTask的任务是可以取消的:
mayInterruptIfRunning是否中断任务的执行,如果为true则会打断计算任务,如果为false,任务会继续执行,只是不再关心它的结果了。
任务只有在NEW状态下才能被取消。
任务取消后,会唤醒所有的等待线程,告诉他们别再等了。
/*
取消任务:
mayInterruptIfRunning:是否中断任务的执行?
true:中断工作线程,任务停止。(只是修改线程的interrupt标记,是否响应中断需要自己实现。)
false:任务取消后,工作线程继续执行。
*/
public boolean cancel(boolean mayInterruptIfRunning) {
/*
state为NEW状态,且CAS改为INTERRUPTING/CANCELLED成功才返回true。
*/
if (!(state == NEW &&
UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
// 任务已经计算完成,或被取消,计算异常,被中断都算取消失败。
return false;
try { // 防止调用interrupt()抛异常,捕获一下
if (mayInterruptIfRunning) {
// 如果需要中断,则将工作线程interrupt()
try {
Thread t = runner;
if (t != null)
t.interrupt();
} finally {
// 将状态改为已中断
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}
}
} finally {
// 任务取消后,需要唤醒阻塞节点
finishCompletion();
}
return true;
}
其他方法
isDone() 任务是否完成:
/*
任务是都完成/结束:不是新建状态都算.
*/
public boolean isDone() {
return state != NEW;
}
isCancelled() 任务有没有被取消:
/*
任务有没有被取消:主动取消,计算异常,被中断都算任务取消。
*/
public boolean isCancelled() {
return state >= CANCELLED;
}
总结
FutureTask的源码不算太难,阅读相对比较轻松。
它用一个int类型的state变量来标记任务的状态,默认是NEW,在计算完成之前一直都是NEW,也只有这个状态下的任务才能被取消。
如果还没计算完毕,线程调用get()会被阻塞,FutureTask会创建一个和线程绑定的WaitNode节点,并采用头插法的方式添加到链表的头部,并将其Park。
COMPLETING代表任务已经计算完毕,只是还没保存结果,马上就快好了,这个状态下线程调用get()是不会被Park的,而是Thread.yield()暂时让出CPU的执行权,过会儿再来看看。
之后要么是计算成功完成,获得计算结果,要么是异常,要么被中断了,不论如何任务都算是结束了,都需要唤醒所有的等待线程。