大家好,我是茄子。今天讲讲Future。
JDK环境:1.8.0_251,系统环境:win10
前言
本文编写思路是先使用一下Future机制,然后探究各个方法的底层原理,最后总结一下。
看这篇文章需要了解的内容有:Unsafe、LockSupport、CAS、volatile
为什么要有 Future 机制?
通过实现 Runnable 接口创建多线程时,Thread 类的作用就是把 run() 方法包装成线程执行体。那么是否可以直接把任意方法都包装成线程执行体呢?Java以前不行!但 Java 的模仿者 C# 可以( C# 可以把任意方法包装成线程执行体,包括有返回值的方法)。
也许受此启发,从 Java 5 开始,Java 提供了 Callable 接口,Callable 接口提供了一个 call() 方法可以作为线程执行体,但 call() 方法比Runable 的 run() 方法功能更强大。
- call() 方法可以有返回值。
- call() 方法可以声明抛出异常。
因此,我们完全可以提供一个 Callable 对象作为 Thread 的 target,而该线程的线程执行体就是该 Callable 对象的 call() 方法。问题是:Callable 接口是 Java 5 新增的接口,而且它不是 Runnable 接口的子接口,所以 Callable 对象不能直接作为 Thread 的 target。而且 call() 方法还有一个返回值—— call() 方法并不是直接调用,它是作为线程执行体被调用的。那么如何获取 call() 方法的返回值呢?Java 5 提供了 Future 接口来代表 Callable 接口里 call() 方法的返回值,并为 Future 接口提供了一个 FutureTask 实现类,该实现类实现了 Future 接口,并实现了 Runnable 接口——可以作为 Thread 类的 target。
简单使用
接下来我们简单使用一下
public class Calculate {
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(1);
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
Thread.sleep(10);
sum += i;
}
return sum;
}
});
System.out.println("主线程等待计算结果。。。");
System.out.println("计算结果是:" + future.get());
// 关闭线程池
executorService.shutdown();
}
}
计算的时候模拟耗时,主线程会阻塞直到获取结果为止。这里的计算只是一种操作。实际业务中可能是更复杂的计算或远程请求等等。
FutureTask源码解析
先看一下 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;
// callable对象,执行完后置空
private Callable<V> callable;
// 要返回的结果或要引发的异常来自 get() 方法
private Object outcome; // non-volatile, protected by state reads/writes
// 执行Callable的线程
private volatile Thread runner;
// 等待线程的一个链表结构
private volatile WaitNode waiters;
可以注意到 state、runner、waiters 都是用 volatile 关键字修饰的。所以它们的变化对其他线程可见。
submit方法
//java.util.concurrent.AbstractExecutorService
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
public class FutureTask<V> implements RunnableFuture<V> {
//...
}
当我们在线程池中执行 Callable 类型的任务的时候,其实就是封装成一个 FutureTask 对象去执行。FutureTask实现了RunableFuture接口,这个RunableFuture接口继承了Runable和Future接口。
classDiagram
class Runnable {
<<interface>>
}
class Callable {
<<interface>>
}
class Future {
<<interface>>
}
class RunnableFuture {
<<interface>>
}
class FutureTask{
}
Runnable <|-- RunnableFuture
Future <|-- RunnableFuture
RunnableFuture <|.. FutureTask
Callable <|.. FutureTask
run方法
public void run() {
// 只有当状态为新建且当前FutureTask无线程执行的时候才可以继续。
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 {
// 执行call方法,并获取返回结果
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
// 如果执行过程出现异常,则将异常赋值到outcome上
setException(ex);
}
//如果正常执行完成,则将result赋值到outcome上
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)
handlePossibleCancellationInterrupt(s);
}
}
可以看到当正常执行完成后会执行 set 方法。异常会走 setException 方法。
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL);
finishCompletion();
}
}
如果任务没有被取消,将 future 执行完的返回值赋值给 result 结果。FutureTask 任务的执行状态是通过 CAS 的方式进行赋值的,并且由此可知,COMPLETING 其实是一个瞬时状态。当将线程执行结果赋值给 outcome 后,状态会修改为对应的 NORMAL,即正常结束。
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL);
finishCompletion();
}
}
过程同上类似,将异常赋值给 outcome 后,状态会修改为对应的 EXCEPTIONAL。
get方法
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 方法的时候,如果状态小于 COMPLETING,就会通过awaitDone方法等待。带超时时间的get方法,如果在指定时间后,状态还是小于 COMPLETING,说明任务超时,抛出异常。
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;
}
// 准备完成,通过yield方法再等等
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
else if (q == null)
// 如果q为空,说明等待队列中没有节点,那么就创建第一个节点
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);
}
}
可以看到如果设置截止日期,那么会判断到截止日期前的时间,如果时间到了,就把等待节点从等待队列中移除,并返回state;如果时间没到就会再阻塞一段时间,再循环判断;如果没有设置截止日期,等待结果的线程就会一直阻塞。那么当任务执行完成会发生什么呢?还记得我们提到过在 run 方法中如果正常完成会执行 set 方法吗?我们再回头看一下。
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
可以看到在 set 方法中执行了 finishCompletion 方法
private void finishCompletion() {
// 循环等待队列,将节点中的线程依次通过unpark命令唤醒。
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 方法中会把在 awaitDone 方法中等待结果的线程统统唤醒(unpark),因为需要的结果出来了,可以拿结果去做其他事情了。等待结果的线程再下一次循环的时候发现 s > COMPLETING,说明任务执行完成了(不管有没有问题),就把 s 给返回了。然后就会执行 report 方法了。
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 说明任务正常执行完成,将结果返回;被取消就抛出被取消的异常;否则就抛出执行中遇到的异常,比如空指针异常、算术运算异常。
setException 方法和 set 方法 思路一致,就不再赘述了。
cancel 方法
Future接口提供了cancel方法,允许取消任务。
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;
}
我们可以在取消任务的时候通过参数来决定是否让线程中断。线程中断不代表立刻停止线程,而是修改线程的中断状态标识(中断状态由false变成true),由线程自行决定如何处理。
removeWaiter 方法
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 的节点。队列中存在一个节点和这个 node 是同一个引用。node 的 thread 置为空了,队列中也就能找到,找到后用后一个节点覆盖前一个节点,完成删除。
首先 q 指向队列的头节点,s 指向 q 的下一个节点,pred 指向 q 的上一个节点。如果 q 的 thread 不为空,就赋值给 pred,q 继续向后走。如果 q 为空,说明 q 节点是需要移除的,那么就用后一个节点覆盖这个节点(pred.next = s)。
但也会遇到一个问题,就是向后遍历的过程中,别的线程因为中断或超时也执行了 removeWaiter 方法,而 node 正好又是和这里的 pred 是同一个引用,那么就需要重新遍历整个等待队列了(retry)。
如果队列的第一个节点正好 thread 为空,此时 q 是头节点,pred 为空,s 是头节点的下一个节点,那么就可以使用 s 替换头节点,所以在第16行可以看到CAS。
再谈 run 方法
在 run 方法中的 finally 代码块中有这两行代码
public void run() {
// ...
try {
// ...
} finally {
runner = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
为什么要有 runner = null 这行代码呢? 在 run 方法入口处,只有 runner 为空时,才能抢锁进入,防止并发执行。任务执行执行完成了,runner 字段可以为空了,不然引用着一个线程,可能会产生内存问题。
handlePossibleCancellationInterrupt方法内容如下:
private void handlePossibleCancellationInterrupt(int s) {
if (s == INTERRUPTING)
while (state == INTERRUPTING)
Thread.yield();
}
为什么要执行 handlePossibleCancellationInterrupt 方法呢?
主要是处理因cancel方法引起的中断。
cancel 方法可以选择传入true表示,如果任务还在运行那么调用运行任务线程的 interrupt 方法进行中断,如果是调用 cancel 的线程还没有完成中断那么当前运行的线程会让步。为什么这么做?我们上面说到过,A线程运行任务,B线程cancel任务,B中断线程A其实是需要时间的,B会先修改任务状态为 INTERRUPTING,然后中断线程A,然后修改状态为 INTERRUPTED 并唤醒等待的线程,从INTERRUPTING - > INTERRUPTED 这段时间,线程A只需要让出cpu等待即可,就不用浪费cpu啦。
通过代码中最初的注释可以看出,设计者的想法是清除掉执行线程的中断标识,但是又认为有中断标识可以用作通信,所以只是安静地让出cpu,等待状态的改变。
总结
Future机制的基本原理
基于Future机制的异步编程模型可以分为以下几个步骤:
- 创建一个 Future对象,并把异步任务提交给一个线程池进行处理。
- 主线程立即返回,并可以继续执行其他操作。
- 线程池中的线程进行异步计算,直到计算完成。
- Future对象可以通过调用
get()方法获取异步计算的结果,或者捕获由get()抛出的异常来获取异常信息。 对于阻塞的操作来说,Future.get()方法非常重要。它可以用来阻塞主线程,直到异步操作的结果返回为止。如果异步操作在主线程调用Future.get()方法之前已经完成,那么调用Future.get()方法会立即返回异步操作的结果;如果异步操作还没有完成,那么调用Future.get()方法会等待异步操作完成,并返回异步操作的结果。
Future机制的阻塞与唤醒原理
在 awaitDone 方法中,如果线程没有被中断,任务的状态还是 NEW,那么第一次循环创建一个WaitNode,WaitNode有当前线程的引用。第二次循环会加入等待队列。第三次循环会通过LockSupport的park方法阻塞自己。等removeWaiter 或者 finishiCompletion 方法执行的时候,会从 WaitNode 中拿到线程的引用,再调用unpark方法唤醒。
参考材料
- 疯狂Java讲义 (豆瓣) (douban.com)
- 并发编程-FutureTask解析 | 京东物流技术团队(juejin.cn/post/726015…)
- JUC源码学习笔记7——FutureTask源码解析,人生亦如是,run起来才有结果 - Cuzzz - 博客园 (cnblogs.com)