FutureTask对象可以接收其他线程的执行结果,将同步操作改为异步操作,从而提高服务的响应时间和吞吐量。那么FutureTask底层使用了LockSupport实现线程间的通信,那么FutureTask是如何获取其他线程的执行结果,又是如何取消任务的执行。本节将通过其源码进行原理的剖析
FutureTask的使用
- 配合线程池使用
用于接收线程池提交执行任务的返回值,并通过get方法获取任务的执行结果。
//创建固定大小的线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 0,
TimeUnit.SECONDS, new LinkedBlockingQueue(100));
//获取执行任务的结果,Task是实现callable的类
Future future = executor.submit(new Task());
//此处可以处理其他的业务逻辑
//获取任务的执行结果
future.get();
- 作为线程的执行对象
由于FutureTask实现了Runnable接口,因此FtureTask可以作为Thread对象的参数,执行任务
//创建FutureTask实例
FutureTask futureTask = new FutureTask(new Task());
//执行FutureTask
new Thread(futureTask).start();
FutureTask任务的状态
FutureTask存在7种状态,其状态转换如下图所示
其中:图中的实线的颜色不同代表不同的线程,任务的执行和任务的取消不是同一个线程,FutureTask任务有四种结束状态normal、exceptional、cancelled、interrupted。当执行get()方法获取结果时,如果不为这四种状态,操作会被阻塞,如果是这四种状态,则根据状态不同返回不同的结果。
- new: 新建一个FutureTask实例时的状态
- completing: 该任务(FutureTask对象被执行,调用run()方法)被执行,run()方法执行完之后的状态
- normal: run()方法,正常执行完
- exceptional: run()方法,抛出异常
- cancelled: 调用任务的cancel(false)方法
- interrupting: 调用任务的cancel(true)方法的中间状态,后续讲解与interruputed的不同
- interrupted: 调用cacel(true)方法时,中断执行任务的线程
每一个状态对于一个int值,源码如下:
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;
2、3、4、6为终止状态
FutureTask的成员变量
- callable
run()方法中执行的任务是指执行的是该对象的call()方法,因为FutureTask主要就是用来接收任务的执行结果的,所以在构建FutureTask实例时,需要传入callable对象 - outcome
存放任务执行结果的,类型为Object类型,可以是正常的结果,也可以是抛出的异常,任务执行完成后赋值 - runner
任务的执行线程,执行run()方法时赋值,只会有一个线程成为runner - waiters
调用get()方法阻塞的线程会被构造为一个节点,加入到链表waiters中,waiters是链表的头结点(并不是执行get()方法的线程都会被加入到该链表中,下面会具体讲解)
节点定义如下:
static final class WaitNode {
volatile Thread thread;
volatile WaitNode next;
WaitNode() { thread = Thread.currentThread(); }
}
存储了线程和下一个节点信息,存储线程主要是为了知道唤醒哪一个线程
从FutureTask的成员变量中可以看出,当实例化一个FutureTask对象时,必须传入一个callable对象,即便传入的是runnable对象,最终也会转变为callable对象
FutureTask构造函数
上面已经提及实例化FutureTask对象,必须传入callable对象,当然也可以传入runnable对象
public FutureTask(Callable callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
//这种用的较少
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
从构造函数中可以看出,构造函数只是做了两件事情,添加任务和初始化状态
FutureTask的任务执行run()
线程池在调用submit()方法时,会将任务封装为一个FutureTask对象,而这个对象就是返回值,拿上面提到的代码举例:
//创建固定大小的线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 0,
TimeUnit.SECONDS, new LinkedBlockingQueue(100));
//获取执行任务的结果,Task是实现callable的类
Future future = executor.submit(new Task());
//此处可以处理其他的业务逻辑
//获取任务的执行结果
future.get();
其中, executor.submit(new Task())执行的操作相当于,new FutureTask(new Task());然后将该对象返回赋值给future,这样通过FutureTask同时维护了任务和结果两个属性。
我们先来看一下run()方法的主体逻辑,源代码如下
public void run() {
//1. 执行任务前的状态检查与设置runner线程
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
//2. 执行任务
Callable c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
//3.异常结果
setException(ex);
}
if (ran)
//3. 正常执行结果
set(result);
}
} finally {
runner = null;
int s = state;
//5. 处理INTERRUPTING状态,就是等待一下
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
正如代码中注释一样,run()主要做了5件事,run()方法中主要涉及任务状态转化图中的黑色实线内容,下面我们一点点来剖析这5部分:
(1)运行任务前的状态检查
- 任务状态必须为new
如果不为new,说明任务已经运行,直接返回 - 如果任务状态为new,使用CAS操作设置该任务的执行线程给runner(CAS操作为原子操作)
如果设置不成功,说明runner的值不为null,也就是说其他线程已经在执行该任务
如果设置成功,则进行后续操作
(2)执行任务
- 再次验证任务状态
因为其他线程,在步骤1、2之间可能会取消该任务,所以再次判断 - 执行任务
调用callable的call()方法
(3)结果处理
- 正常结果处理
其源代码如下:
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
- 使用原子操作将状态修改为completing,表示任务执行完,但是还没有给outcome赋值
- 给outcome赋值
- 使用原子操作修改状态为normal,表示正常结束
- 完成后续操作,唤醒阻塞的线程(该操作在第4步讲解)
- 异常结果处理
源代码如下:
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
除了状态转换不同,其他的操作与正常结果处理完全一致
(4) 唤醒阻塞线程
- 首先使用原子操作CAS设置waiters为null
- 依次遍历等待链表上的节点,使用LockSupport的unpark()方法唤醒线程
- callable置空
其源代码如下:
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;
//唤醒每一个等待节点中的线程t
//t就是调用get方法的线程
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
}
(5) 等待interrupting状态的改变
run()方法的finally语句内容,因为interrupting状态不为最终状态,等待其变为最终状态
主要是因为在此时,该任务被其他线程取消,调用了cancel(true)方法,只需中断一下runner即可将状态修改为interrupted, 其操作如下所示:
private void handlePossibleCancellationInterrupt(int s) {
// 如果是INTERRUPTING状态,则表明其他的线程在取消该任务,调用runner的interrupt方法后,会
//修改状态为INTERRUPTED状态,这里等待只是为了等待其他的线程操作完成
if (s == INTERRUPTING)
while (state == INTERRUPTING)
Thread.yield(); // wait out pending interrupt
}
FutureTask的get()方法
FutureTask提供了两种获取任务执行结果的方法
- get() 阻塞方法,会一直阻塞到任务执行完成,才会返回
- get(long timeout, TimeUnit unit) 最长阻塞timeout的时间,即便任务没有执行完,也会返回
get()方法很简单,主要做了两件事情:
- 判断任务状态,如果不为四种终止状态,则执行awaitDone()方法
- 任务结束,解析任务状态,返回结果
其源代码如下所示:
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
//如果结果没有准备好,该方法会一直阻塞
s = awaitDone(false, 0L);
//执行到这,说明awaitDone方法已经返回, 也就是说结果准备好了
return report(s);
}
从源码中可以看出,主要的操作就是awaitDone()和reposrt(),接下来我们一个一个讲解
- awaitDone()
两个获取任务结果的方式(阻塞与超时阻塞)都调用了awaitDone()方法,其主要逻辑如下,对应源码一起看:
- 根绝超时时间计算截止时间
- 定义等待节点,如果需要构造等待节点(是有条件的),则构造并赋值给q
- queued用于标记该阻塞节点是否已经插入到了链表中,防止多次插入
- 使用死循环进行操作,(不达目的不罢休,达到目的才会break出循环)
- 判断当前线程(注意,不是runner,而是当前线程,获取任务执行结果的线程)是否被中断,如果中断,则移出该线程的阻塞节点(removeWaiter()方法,如果没有阻塞节点,该方法直接返回),并抛出中断异常
- 如果任务已经执行完成,将等待节点的线程置空,返回状态
- 如果任务已经执行完,但还没有给outcome赋值,则放弃cpu资源等待
- 如果任务还没有执行(状态为new),构造等待节点
- 如果任务还没有执行,并且存在等待节点,将等待节点加入到等待链表waiters中,通过CAS操作采用头插法插入节点
- 如果设置了超时,判断有无超时,如果没有设置超时,则阻塞该线程,等待任务执行完执行finishCompletion()方法唤醒
- 如果设置了超时,判断有无超时,如果超时,移出等待节点,返回状态
- 如果没有超时,调用LockSupport的parkNanos()方法进行超时等待
源码如下:
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
//是否设置超时,如果设置了,计算截止时间
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
//是否插入waiters队列中,用来保证一个节点只插入一次
boolean queued = false;
for (;;) {
//如果当前执行get方法的线程中断了,从waiters中移出该线程的等待节点
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
//如果状态为COMPLETING,则为完成状态,直接返回状态即可
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
//如果执行完了,正在设置结果,则放弃时间片轮询等待
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
//任务状态为NEW, 构造等待节点
else if (q == null)
q = new WaitNode();
//状态为NEW, 并且节点不为null,并且该节点没有加入到waiter队列中
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);
}//如果没有设定超时,则阻塞,等待runner线程执行完任务之后被唤醒
else
LockSupport.park(this);
}
}
从源码中可以看出,并不是所有的调用get()方法的线程,就会进入阻塞链表,只有在调用get()方法时,任务状态为new的线程才会加入阻塞链表waiters中,等待任务执行完唤醒。如果任务执行完了,表明结果会快就会准备好,只需要自旋等待即可。
在上述方法中,存在removeWaiter()方法,我们不妨也看一下:
- 判断传入的节点是否为null,如果为null直接返回
- 如果不为空,首先将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) // check for race
continue retry;
}
else if (!UNSAFE.compareAndSwapObject(this, waitersOffset,
q, s))
continue retry;
}
break;
}
}
}
其上删除逻辑就是寻找thread字段为null的节点的前节点,因为thread节点为null的就是我们要删除的节点。如果前节点存在,直接删除该节点,如果前节点不存在,说明该节点在链头,因为链表的插入操作采用的是头插法,因此,修改头结点会有线程安全问题,所以使用线程安全的CAS设置头结点的值,从而达到删除该节点的目的。如果原子操作没有删除成功,说明链表的结构发生了变化,需要重试。
- 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);
}
执行到report()方法时,任务状态到了终止状态
- normal为正常状态
- 大于等于cancelled代表取消的两个状态:取消和中断
- 剩下的状态为异常状态
FutureTask的get(long timeout, TimeUnit unit)方法
与get()方法对比,除了参数检查外,处理了一下超时,如果超时结果还没有准备好则抛出超时异常
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);
}
FutureTask的cancel方法
该方法存在一个boolean类型的参数:
- true:表示将任务状态修改为interrupted的终止状态,并且调用runner的interrupt()方法
- false:只将任务状态修改为cancelled
源代码如下:
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;
}
该操作对应状态转换图中的红线部分
只能取消状态为new的任务(没有执行,或者正在执行都可能是new),如果任务状态不为new(任务执行完成了),则直接返回false,并不能让正在执行的任务终止,而是只能让没有执行的任务不执行。
runner线程的中断操作,并不能左右执行线程,因为中断操作只是一个信号,如果执行线程不响应该信号,则该中断有没有是一样的,除非执行线程中存在影响中断的操作,否则,即便调用了interrupt()方法也不起任何作用
FutureTask其他方法
runAndSet()方法,执行任务后将任务的状态修改为new,适用于多次执行的任务