FutureTask
是Java中的一个类,位于java.util.concurrent
包中。它实现了RunnableFuture
接口,同时也是Runnable
和Future
的子类,因此可以作为一个任务提交给线程池执行,并且可以获取任务的执行结果
下面从源码角度分析一下 FutureTask
这个类:
构造函数
FutureTask
有以下两个构造函数:
ini
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // 初始状态为 NEW
}
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // 初始状态为 NEW
}
其中,第一个构造函数接收一个 Callable<V>
对象作为参数,表示要执行的异步任务;第二个构造函数接收一个 Runnable
和一个初始结果 V
作为参数,它会将 Runnable
封装成一个 Callable
对象进行执行,并返回结果。
状态转换
FutureTask
有下面这些状态:
arduino
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
,并保存执行结果 - 如果任务执行异常,状态会变为
EXCEPTIONAL
,并保存异常信息 - 如果任务被取消,状态会变为
CANCELLED
- 如果任务正在执行中,但被中断了,状态会变为
INTERRUPTING
,并尝试中断任务 - 如果任务被中断成功,状态会变为
INTERRUPTED
执行任务
FutureTask
的核心是执行任务,它会根据任务的状态来决定如何执行:
ini
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);
}
}
方法主要做了以下几件事:
- 判断当前任务是否处于初始状态,并且使用 CAS 操作将当前线程设置为执行线程,如果设置失败,则说明已经有其他线程在执行任务了,返回
- 获取任务的
Callable
对象,如果任务状态不是NEW
,则直接返回 - 使用
Callable
执行任务,如果执行成功,使用set
方法保存结果;如果执行异常,使用setException
方法保存异常信息 - 清除执行线程,并根据任务状态决定是否需要处理取消中断的情况
保存结果
kotlin
protected void set(V v) {
if (U.compareAndSwapInt(this, STATE, NEW, COMPLETING)) {
outcome = v;
U.putOrderedInt(this, STATE, NORMAL); // final state
finishCompletion();
}
}
方法主要通过原子操作进行状态的变更,如果之前是NEW,则将其通过compareAndSwapInt
变为COMPLETING。然后进行结果的赋值,赋值成功后,通过putOrderedInt
将状态设置为NORMAL
。
为什么一开始使用compareAndSwapInt
,然后又使用了putOrderedInt
?其实人家设计者一定有自己的考虑,这里从成本开销简单看一下:
首先 ,compareAndSwapInt
方法的成本消耗主要包括两个方面:
- 内存访问成本:compareAndSwapInt方法需要读取和写入内存中的数据,这涉及到CPU与内存之间的数据传输和缓存同步,因此会产生一定的内存访问成本。
- CPU指令成本:compareAndSwapInt方法在底层使用了CPU的CAS(Compare-And-Swap)指令来保证操作的原子性。CAS指令需要执行多条汇编指令,包括读取内存、比较值、更新值等操作,因此会产生一定的CPU指令成本。
总的来说,compareAndSwapInt
方法的成本消耗相对较高,但是它提供了一种可靠地在多线程环境下进行原子操作的机制,以确保线程安全性。
然后 ,putOrderedInt
方法的成本消耗主要包括两个方面:
- 内存访问成本:
putOrderedInt
方法需要写入内存中的数据,这涉及到CPU与内存之间的数据传输和缓存同步,因此会产生一定的内存访问成本。 - 缓存一致性成本:
putOrderedInt
方法不保证立即对其他线程可见,这意味着其他线程可能无法立即看到该字段的更新。在多核处理器上,为了保持缓存一致性,需要通过内存屏障指令或者其他机制来确保对其他线程的可见性。这样的操作会引入额外的开销(例如内存屏障,就是禁止指令重排,会带来额外的处理器开销,毕竟指令重排将顺序处理下一条应该执行的命令变为处理下一条能够执行的命令,有提高处理器的能力)。
总的来说,putOrderedInt
方法的成本消耗相对较低,比起compareAndSwapInt
等具有原子性保证的方法,它不需要执行CAS指令,因此在性能上可能更加高效。
最后回到问题本身,在if判断中,一定要使用原子性的操作来保障判断是没有问题的,否则多线程并发问题会直接影响逻辑。在赋值的时候,不使用CAS可以节约开销,而且延迟同步的方式也在可接受范围内,毕竟COMPLETING
是一个中间态,而中间态是瞬时的,其他状态都是最终态,不会再做什么变动。
获取结果
FutureTask
实现了 Future
接口,因此可以使用 get
方法获取任务执行结果:
ini
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
方法会一直阻塞直到任务执行完成,并返回执行结果或者异常信息;get(timeout, unit)
方法会阻塞等待一定时间,如果任务未能在指定时间内完成,会抛出 TimeoutException
。
再往细节上面掰扯一下:awaitDone
方法
ini
/**
等待完成或在中断或超时时中止。
@param timed 如果使用定时等待,则为true
@param nanos 如果使用定时等待,则为等待的时间
@return 完成时的状态或超时时的状态
*/
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
//对于每次调用park,仅调用一次nanoTime,如果nanos <= 0L,则立即返回,无需分配或调用nanoTime,如果nanos == Long.MIN_VALUE,则不会发生下溢,如果nanos == Long.MAX_VALUE,并且nanoTime不是单调的
long startTime = 0L;
//这是一个等待的节点,结构中包含Thread的能力
WaitNode q = null;
boolean queued = false;
//get方法阻塞,一直等待获取到完成结果或者异常状态
for (;;) {
int s = state;
//如果完成了就返回值,并且置空等待节点
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
//如果处于中间状态,则释放CPU,再来一次,毕竟中间态是一个瞬时态
else if (s == COMPLETING)
Thread.yield();
//线程被置于中断态,则抛异常
else if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
//执行到这里,表示是第一个线程刚进来
else if (q == null) {
if (timed && nanos <= 0L)
return s;
q = new WaitNode();
}
else if (!queued)
queued = U.compareAndSwapObject(this, WAITERS, q.next = waiters, q);
//是否超时,超时则异常,否则在未超时时间内等待返回结果
else if (timed) {
final long parkNanos;
if (startTime == 0L) { // first time
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 may be slow; recheck before parking
if (state < COMPLETING)
LockSupport.parkNanos(this, parkNanos);
}
else
LockSupport.park(this);
}
}
再看一下结果是如何返回的:report
方法,逻辑比较简单,就是根据状态返回不同的值
java
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);
}
取消任务
FutureTask
还实现了取消任务的方法:
java
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
,它会使用 CAS 操作将任务状态改为 INTERRUPTING
或 CANCELLED
,并根据 mayInterruptIfRunning
参数决定是否要中断正在执行的任务。如果成功取消了任务,会调用 finishCompletion
方法来完成任务的清理工作。
总结
通过以上的分析,我们可以看出,FutureTask
是一个非常强大、灵活、可扩展的类,它不仅可以用于异步执行任务和获取结果,还支持任务取消、超时等功能,并且使用了很多并发编程中常用的技巧和设计模式。FutureTask
的源码实现比较简洁、易于理解