推荐阅读
JUC源码
并发工具
19 | CountDownLatch和CyclicBarrier让多线程步调一致
17 | ReadWriteLock:如何快速实现一个完备的缓存?
在上一篇文章 23 | Future:获取线程的执行结果 中,我们介绍了使用 Future来获取异步任务的执行结果。
Future是什么?
在并发编程中,我们经常使用异步任务、来提高程序性能;多线程的三种实现中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。
通过实现Callback接口,结合Future可以来接收多线程的执行结果。
Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。
Future 接口有 5 个方法:
cancel():取消任务的方法
isCancelled():判断任务是否已取消的方法
isDone():判断任务是否已结束的方法
get():以阻塞方式获取任务执行结果,如果任务还没有执行完,调用get(),会被阻塞,直到任务执行完才会被唤醒。
get(timeout, unit):支出超时机制获取任务执行结果
通过 Future 接口的这 5 个方法,我们提交的任务不但能够获取任务执行结果,还可以取消任务、判断是否完成。
我们使用线程池 ThreadPoolExecutor 执行任务时,调用submit()方法向线线程提交任务。
submit表示提交一个任务,返回值类型是Future;返回后,只是表示任务已提交,不代表已执行,通过Future可以查询异步任务的状态、获取最终结果、取消任务等。
Future是一个重要的概念,是实现"任务的提交"与"任务的执行"相分离的关键,是其中的"纽带",任务提交者和任务执行服务通过它隔离各自的关注点,同时进行协作。
FutureTask
Future的主要实现类是FutureTask。
FutureTask 实现了 Runnable 和 Future 接口,由于实现了 Runnable 接口,FutureTask就是一个有结果可期待的任务。
FutureTask相关的使用,可移步 23 | Future:获取线程的执行结果 温习。
FutureTask构造器 要传入Callable<V> callable,FutureTask本质就是包装了callable,并提供 取消任务执行、检测是否完成、获取执行结果 的能力。
我们提交到线程池 ThreadPoolExecutor的任务,可以轻松的使用Future 获取结果。使用示例如下:
// 创建FutureTask
FutureTask<Integer> futureTask
= new FutureTask<>(()-> 1+2);
// 创建线程池
ExecutorService es =
Executors.newCachedThreadPool();
// 提交FutureTask,FutureTask本身就是一个任务
es.submit(futureTask);
// 获取计算结果
Integer result = futureTask.get();
需要注意的是:get() 方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用 get() 方法的线程会阻塞,直到任务执行完才会被唤醒。
上一篇 手写FutureTask 我们猜想了FutureTask的实现,运用 Object.wait/notify等待通知机制,手写了一个简单版的FutureTask,能够让futureTask.get(); 阻塞获取结果。
今天对比JDK的FutureTask实现,我们来看看并发大神 Doug Lea 是如何设计的。
FutureTask源码
1.Future的创建
我们使用多线程异步执行任务时,通常是使用线程池,而线程池 ThreadPoolExecutor#submit 方法返回的就是Future。下面就从这里入手,探究FutureTask的源码。
ThreadPoolExecutor#submit
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
内部调用newTaskFor生成了一个RunnableFuture,RunnableFuture是一个接口,既扩展了Runnable,又扩展了Future,没有定义新方法。
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
可见,ThreadPoolExecutor#submit 提交的Callable任务,最终被包装成了FutureTask 。
FutureTask是Future的子类实现, 又是Runnable的实现. 因此, 它既可以执行又可以获取结果。
2.Future的执行逻辑
创建FutureTask时, 会把我们提交的任务 (Callbable
)传递给FutureTask. 其实FutureTask执行时, 会委托给传进来的Callable
. 基本逻辑如下:
public class FutureTask<V> implements RunnableFuture<V> {
// 变量state表示状态
private volatile int state;
int NEW = 0; //刚开始的状态,或任务在运行
int COMPLETING = 1; //临时状态,任务即将结束,在设置结果
int NORMAL = 2; //任务正常执行完成
int EXCEPTIONAL = 3; //任务执行抛出异常结束
int CANCELLED = 4; //任务被取消
int INTERRUPTING = 5; //任务在被中断
int INTERRUPTED = 6; //任务被中断
// 用来存储 "用户提供的有实在业务逻辑的" 任务
private Callable<V> callable;
// 用来保存异步计算的结果
private Object outcome;
public FutureTask(Callable<V> callable) {
if (callable == null) throw new NullPointerException();
// 保存外部传来的任务, 待会在run()方法中调用
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
public void run() {
// 省略其他代码 ...
Callable<V> c = callable;
//FutureTak的执行逻辑委托给用户提供的真正任务
V result = c.call();
// 设置异步任务结果
set(result);
}
// 其他代码省略 ...
}
如果只关心主流程,其实和昨天我们 手写FutureTask 的实现没什么区别,就是执行异步任务,执行完成后设置执行结果。
需要注意的是,futureTask.get(); 阻塞获取结果,是要等待异步任务执行完才行,为此FutureTask 声明了state变量,来代表当前任务的状态。
3.FutureTask是如何保存计算结果的
FutureTask内部用了一个Object成员outcome来存储异步任务的结果. run() 方法调用用户传过来的Callbable的call()方法并产生一个运算结果, 此时会调用set()方法来将计算结果存储到成员outcome中. 下面就来看看FutureTask是如何将计算结果设置到outcome成员中的:
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
需要注意的是, 赋值之前有一个任务状态的切换,就是将FutureTask 声明的state修改成COMPLETING,表示即将结束。
这个切换会同步,因此设置完后, 其他线程就可以立即看到最新的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
}
finishCompletion()
方法,代表异步任务真正结束。
如果结束之前,提交任务的线程调用futureTask.get(); 来获取结果,只能将其阻塞,获取结果的线程被阻塞后,用WaitNode封装。
static final class WaitNode {
volatile Thread thread;
volatile WaitNode next;
WaitNode() { thread = Thread.currentThread(); }
}
WaitNode 用next引用 构成了单向链表,意味着多线程环境,提交的一个异步任务,如果多个相关线程都关心其执行结果,都可以调用futureTask.get(); 来获取结果
如果 在任务完成之前就来获取结果,就用 WaitNode 单向链表维护,在任务结束(finishCompletion()方法),会唤醒WaitNode 链表中等待结果的线程。
到这里,我们梳理了 FutureTask 源码的主线,从 ThreadPoolExecutor#submit 提交任务、创建FutureTask 开始,到FutureTask代表的任务执行,执行结束后赋值结果、与唤醒等待结果的线程。
除了这条主线,还有一条线,就是 FutureTask 支持取消任务,还能判断是否完成,和阻塞获取结果。
5. 取消任务
上一篇 手写FutureTask 中,我们并没有实现取消任务,并提出 取消任务一种可行的方式是中断当前线程。
之所以没有实现,是因为Java中断机制非常复杂,Java线程的中断 interrupt() 和OS实现有关。
实际上,JDK的 FutureTask 取消任务,也是 interrupt()中断执行任务的线程。
还记得FutureTask 是如何创建的吗?
通常是我们使用线程池 ThreadPoolExecutor#submit 提交任务后返回一个Future。
也就是FutureTask所代表的任务,是被线程池里的线程执行的,而线程池 ThreadPoolExecutor 为我们实现了线程的管理,使我们不需要关注线程创建和协调的细节。
那我们如何知道当前 FutureTask 是被线程池里的哪个线程执行呢?取消任务时该中断哪个线程呢?
—— 不能确定是线程池里的哪个线程。
但Java线程Thread,有获取当前执行线程的方法Thread.currentThread();JDK FutureTask 的设计是,在run()方法一开始,保存当前的执行线程;
public class FutureTask<V> implements RunnableFuture<V> {
// 用来存储 "用户提供的有实在业务逻辑的" 任务
private Callable<V> callable;
// 用来保存异步计算的结果
private Object outcome;
private volatile Thread runner;
private static final long runnerOffset;
public void run() {
// 保存当前执行的线程
UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread())
Callable<V> c = callable;
V result = c.call();
set(result);
}
// 其他代码省略 ...
}
FutureTask 被线程池里的某个线程执行时,必定去执行run方法;而run方法将执行线程保存到runner,当调用cancel()时,就能interrupt()执行任务的线程。
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;
}
cancel()除了判断某些状态下不可取消外,最重要的就是 t.interrupt();
中断线程后,会将state状态设置为INTERRUPTED
6. 阻塞获取结果
昨天 手写FutureTask 文章中我们运用 Object.wait/notify等待通知机制,来让futureTask.get(); 阻塞获取结果。并提到JDK的FutureTask 使用LockSupport 来阻塞线程,一起来看LockSupport.get()源码:
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
LockSupport.get()基本逻辑是,如果任务还未执行完毕,就awaitDone等待完成;awaitDone()方法较长:
/**
* Awaits completion or aborts on interrupt or timeout.
* @param timed true if use timed waits
* @param nanos time to wait, if timed
* @return state upon completion
*/
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);
}
}
awaitDone()方法有三种结果
如果指定waitNanos,阻塞指定的时间
没有指定waitNanos,则阻塞当前获取结果的线程,加入WaitNode 队列;
如果当前获取结果的线程被中断,抛出InterruptedException异常
前两种结果,都会阻塞线程,都是使用LockSupport.park。
关于LockSupport 后续会深入分析。
LockSupport.get()如果任务还未执行完毕,就等待完成;如果已完成就调用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);
}
判断是否完成、是否取消,因为FutureTask加入了state状态标记,在关键流程都更改了状态,因此判断是否完成、是否取消,比较容易:
public boolean isCancelled() {
return state >= CANCELLED;
}
public boolean isDone() {
return state != NEW;
}
总结
利用多线程可以快速将一些串行的任务并行化,从而提高性能;如果任务之间有依赖关系,比如当前任务依赖前一个任务的执行结果,这种问题基本上都可以用 Future 来解决。
但 Future 本身功能还是有限,尤其是Future.get获取结果,是阻塞式的。为此业界实现了可回调的FutureTask,明日细聊。
此外,对于更复杂的多任务串行、聚合关系,还有CompletableFuture的支持,后续会持续更新。
本文首发于 公众号 架构道与术(ToBeArchitecturer),欢迎关注、学习更多干货~
推荐阅读
JUC源码
并发工具
19 | CountDownLatch和CyclicBarrier让多线程步调一致
17 | ReadWriteLock:如何快速实现一个完备的缓存?