接上一篇《Java并发系列(9)——并发工具类》
文章目录
8 FutureTask 与 CompletionService
把 FutureTask 和 CompletionService 放在一起,并不是因为它们之间有什么特别的联系,虽然确实有一点联系。
主要是因为下一章要讲线程池,如果不把 FutureTask 和 CompletionService 搞清楚,线程池的部分代码看得会比较困惑。
8.1 FutureTask
FutureTask 的实现比较简单,但它是一个非常基础的东西。只要涉及到有返回值的异步调用,或直接或间接一般都会用到它。
用法很简单,这里给个示例:
package per.lvjc.concurrent.futuretask;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
import java.util.concurrent.TimeUnit;
public class FutureTaskDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
RunnableFuture<String> future = new FutureTask<>(() -> {
System.out.println("run by thread: " + Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(2);
return "success";
});
new Thread(future).start();
long start = System.currentTimeMillis();
String result = future.get();
long end = System.currentTimeMillis();
System.out.println("get result:" + result + ", cost:" + (end - start) + "ms");
}
}
8.1.1 类图
8.1.2 几个问题
FutureTask 实现了 Future 接口的 5 个方法,以及 Runnable 接口的 run 方法。
其实从 FutureTask 的用法很容易猜出,它内部持有一个 Runnable 或 Callable,调用它的 run 方法时会被代理到 Runnable 或 Callable 的 run 方法去。
但还有几个细节问题:
- 怎样 cancel 一个任务?
- 如果任务尚未执行,怎么办?
- 如果任务正在执行,怎么办?
- 如果任务已经执行完了,怎么办?
- 怎样算是 isDone?
- 如果任务尚未执行,算不算?
- 如果任务正在执行,算不算?
- 如果任务执行成功,算不算?
- 如果任务执行抛出未捕获异常,算不算?
- 如果任务尚未执行、正在执行、执行完成被 cancel 了,这几种情况分别算不算?
8.1.3 实现细节
8.1.3.1 属性
共 5 个:
- state:int 类型,从 0 ~ 6 记录了 FutureTask 的 7 种状态;
- callable:Callable 类型,待执行的任务,Runnable 会被包装成 Callable;
- outcome:Object 类型,记录执行结果,即 Callable 的返回值,或者也可能是个 Throwable;
- runner:Thread 类型,记录正在执行的线程;
- waiters:WaitNode 类型,记录正在阻塞等待结果的线程,保存的是链表的第一个节点。
8.1.3.2 run 方法
主干逻辑:
public void run() {
//------1.判断状态------
//1.1.状态不为 NEW,直接 return
if (state != NEW ||
//1.2.状态为 NEW,再尝试将 runner 由 null 改为当前线程
//如果改成功了,往下走执行 Callable 的 run 方法
//如果改失败了,说明被其它线程给改了,return,让改成功的线程去执行
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
//------2.执行 run 方法------
Callable<V> c = callable;
//2.1.再次判断状态,有可能被 cancel
if (c != null && state == NEW) {
V result;
boolean ran;
try {
//2.2.执行 run 方法
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
//2.3.执行异常
setException(ex);
}
if (ran)
//2.4.执行成功
set(result);
}
} finally {
//------3.收尾工作------
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
//3.1.把 runner 再改回 null
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
//3.2.处理可能被遗漏的 interrupt
//因为其它线程在 cancel 的时候是先改状态,再执行 interrupt,
//这就会存在一个线程调度问题,
//状态改成了 INTERRUPTING,随后 cpu 时间片到了,线程被挂起,
//以致于本该到来的 interrupt 被延后,直到任务已经执行完了,interrupt 还没来
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
run 方法异常退出分支,走 setException 方法:
//处理 run 方法异常退出
protected void setException(Throwable t) {
//把状态从 NEW 改成 COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
//把 run 方法抛出的异常赋给 outcome
outcome = t;
//把状态改成 EXCEPTIONAL
//到这里就不需要 cas 了,因为 COMPLETING 是一种安全的状态
//其它线程如果看到状态是 COMPLETING 就不会动了
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
//最后的结束动作
finishCompletion();
}
//如果更改状态失败,直接退出
}
run 方法成功执行分支,走 set 方法:
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
//把 Callable 的返回值赋给 outcome
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
处理可能遗漏的 interrupt:
private void handlePossibleCancellationInterrupt(int s) {
// It is possible for our interrupter to stall before getting a
// chance to interrupt us. Let's spin-wait patiently.
if (s == INTERRUPTING)
while (state == INTERRUPTING)
Thread.yield(); // wait out pending interrupt
// assert state == INTERRUPTED;
// We want to clear any interrupt we may have received from
// cancel(true). However, it is permissible to use interrupts
// as an independent mechanism for a task to communicate with
// its caller, and there is no way to clear only the
// cancellation interrupt.
//
// Thread.interrupted();
}
如果状态是 INTERRUPTING,说明调用 cancel 方法的线程还没有结束,在这里要等它跑完。
这里是一个 while 循环,直到状态被改掉为止,通过 yield 试图把 cpu 让给其它线程,也是希望 cancel 线程能早点拿到 cpu 资源早点跑完。
最后,不管是 setException 还是 set 方法在赋值给 outcome 之后都会调用:
private void finishCompletion() {
// assert state > COMPLETING;
//------1.唤醒因为 get 方法在阻塞的所有线程------
//1.1.外面一层循环是给 cas 失败自旋用的
for (WaitNode q; (q = waiters) != null;) {
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
//1.2.里面一层循环,是遍历链表
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;
}
}
//------2.调用 hook 方法,默认是空实现------
done();
callable = null; // to reduce footprint
}
run 方法总结:
- 主要做三件事:
- 执行任务;
- 保存任务执行结果;
- 唤醒阻塞的线程来拿结果;
- callable 属性:主要就是执行 callable 对象的 run 方法,执行完置为 null,因此只会执行一次;
- runner 属性:控制 Callable#run 方法的逻辑不会被多个线程并发执行;
- outcome 属性:
- Callable#run 方法抛出异常,outcome 属性保存这个 Throwable;
- Callable#run 方法正常退出,outcome 属性保存其返回值;
- waiters 属性:Callable#run 方法执行完毕,不论成功还是异常,outcome 属性赋值后,遍历链表唤醒所有阻塞的线程;
- state 属性:
- 直到 outcome 赋值之前,一直都是 NEW;
- 成功执行,状态变更:NEW -> COMPLETING -> NORMAL;
- 异常执行,状态变更:NEW -> COMPLETING -> EXCEPTIONAL。
8.1.3.3 get 方法
主干逻辑:
public V get() throws InterruptedException, ExecutionException {
int s = state;
//小于等于 COMPLETING 意思是还没执行完成
if (s <= COMPLETING)
//阻塞等待结果
s = awaitDone(false, 0L);
//执行完成,获取结果
return report(s);
}
阻塞逻辑:
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
//死循环自旋
for (;;) {
//1.如果从 get 阻塞中被打断
if (Thread.interrupted()) {
//把当前节点从链表删除
removeWaiter(q);
//抛出打断异常,退出阻塞
throw new InterruptedException();
}
int s = state;
//2.状态值大于 COMPLETING,执行已经有结果了,退出阻塞
if (s > COMPLETING) {
if (q != null)
//这里仅仅把 thread 置空,而没有删除当前节点,
//上面 removeWaiter 方法里面遍历链表时遇到 thread == null 的节点会将其删除
q.thread = null;
return s;
}
//3.状态已经是 COMPLETING,让出 cpu 资源,进入下一次循环
//这里不用创建节点加入链表阻塞,
//因为 COMPLETING 状态表示任务已经执行完,只是还没有保存结果,
//这之间只有几条指令的时间,直接 cpu 空跑即可,
//再创建节点、阻塞、被唤醒,就浪费时间了
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
//4.走到这里,说明状态还是 NEW,创建节点,进入下一次循环
else if (q == null)
q = new WaitNode();
//5.节点已经创建过了,将其加入链表,进入下一次循环
else if (!queued)
//q.next = waiters 而不是 waiters.next = q,
//显然是把新节点插在了头部
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
//6.节点已经入队,如果有超时时间设置,检查是否超时
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
//超时,不管有没有结果,删除当前节点,退出阻塞
removeWaiter(q);
return state;
}
//没有超时,阻塞当前线程
LockSupport.parkNanos(this, nanos);
}
//7.节点已经入队,没有超时设置,阻塞等待唤醒
else
LockSupport.park(this);
}
}
退出阻塞之后,获取执行结果的逻辑:
private V report(int s) throws ExecutionException {
Object x = outcome;
//1.正常退出,返回执行结果
if (s == NORMAL)
return (V)x;
//2.任务被取消,抛异常
if (s >= CANCELLED)
throw new CancellationException();
//3.没有正常结束,也没有被取消,那就是任务执行本身异常,包装异常抛出
throw new ExecutionException((Throwable)x);
}
get 方法总结:
- 用链表保存了所有在 get 方法阻塞的线程;
- 链表采用头插法,后阻塞的线程排在前面,不算超时自己醒来的的线程,会先被唤醒;
- get 会得到三种结果:
- 任务正常完成:得到任务返回值;
- 任务异常退出:抛出 ExecutionException,里面包装了任务抛出的异常;
- 任务被取消:抛出 CancellationException。
8.1.3.4 cancel 方法
主干逻辑:
//取消任务,成功取消返回 true,否则返回 false
public boolean cancel(boolean mayInterruptIfRunning) {
//------1.判断状态------
//1.1.如果状态不为 NEW(由 run 方法可知意为执行已经出结果了),直接 return false
if (!(state == NEW &&
//1.2.如果状态为 NEW,尝试将状态改为 INTERRUPTING 或 CANCELLED,
//如果更改状态失败,意味着状态已经不为 NEW 了,返回 false
//如果更改状态成功,意味着成功取消任务,继续往下走,最终肯定会返回 true
UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
return false;
try { // in case call to interrupt throws exception
//------2.打断线程------
//2.1.根据入参,如果需要将 run 方法线程打断,就执行打断操作
if (mayInterruptIfRunning) {
try {
Thread t = runner;
//NEW 状态的第一种情况:任务已经执行,将其打断
if (t != null)
t.interrupt();
//NEW 状态的第二种情况:任务尚未执行,没有线程可打断
} finally { // final state
//任务是被打断取消的,以 INTERRUPTED 状态结束
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}
}
//2.2.如果不需要打断 run 方法线程,什么都不做
} finally {
//------3.收尾工作:唤醒阻塞线程------
finishCompletion();
}
return true;
}
cancel 方法总结:
- cancel 会失败的情况:run 线程任务执行已经出了结果,不论是正常执行结束或者抛出异常;
- cancel 会成功的情况,又分两种不同的处理逻辑:
- 需要打断:如果任务已经执行,将其打断,最终以 INTERRUPTED 状态结束;
- 不需要打断:如果任务已经执行,放任其不管,最终以 CANCELLED 状态结束;
- 对于尚未执行的任务,以上两种情况,仅最终状态不同,没有其它区别;
- 对于唤醒阻塞线程:
- cancel 成功:需要唤醒;
- cancel 失败:不需要唤醒;
- cancel 方法的影响:
- 如果返回 false,即 cancel 失败:cancel 方法什么都没干,没有任何影响;
- 如果返回 true,即 cancel 成功:cancel(true) 会打断 run 线程,cancel(false) 不会,然后它们都会唤醒阻塞线程,被唤醒的线程 get 执行结果会抛出一个 CancellationException。
8.1.3.5 七种状态汇总
FutureTask 的 7 种状态:
- NEW(0):尚未执行,或正在执行;
- COMPLETING(1):执行完成,还没有保存结果;
- NORMAL(2):正常完成,已保存了任务执行的返回值;
- EXCEPTIONAL(3):异常退出,已保存了任务抛出的异常;
- CANCELLED(4):任务被取消,如果取消的时候,任务还没执行,不会再执行;如果正在执行,让其继续跑下去;
- INTERRUPTING(5):任务正在被取消,如果任务还没执行,不会再执行;如果正在执行,紧接着将其打断(任务是否响应这个打断是另外一回事);
- INTERRUPTED(6):任务已被取消,如果取消的时候,任务还没执行,不会再执行;如果正在执行,那么此时任务已经被打断过了,任务有没有理会这个打断,FutureTask 无法得知。
状态变更的四种情况:
- 正常执行:NEW -> COMPLETING -> NORMAL;
- 执行异常:NEW -> COMPLETING -> EXCEPTIONAL;
- cancel(true) 成功:NEW -> INTERRUPTING -> INTERRUPTED;
- cancel(false) 成功:NEW -> CANCELLED;
- cancel(true/false) 失败:对任务执行没有任何影响,即前两种情况。
状态分类:
- 从是否安全的维度:
- 非安全状态:NEW,所有线程都可以把 NEW 状态改为其它状态,要考虑并发安全问题;
- 安全状态:其它所有状态,一个线程看到状态不为 NEW,要么等待状态再次变更,要么直接返回,不能做任何事,所以不存在并发安全问题;
- 从阶段的维度:
- 初始态:NEW;
- 中间态:COMPLETING,INTERRUPTING;
- 最终态:NORMAL,EXCEPTIONAL,INTERRUPTED,CANCELLED。
8.1.3.6 isCancelled 方法
public boolean isCancelled() {
//包括:CANCELLED,INTERRUPTING,INTERRUPTED
//第一个是 cancel(false) 导致的
//后两个是 cancel(true) 导致的
return state >= CANCELLED;
}
8.1.3.7 isDone 方法
public boolean isDone() {
//包括除 NEW 以外的 6 种状态
//因为 NEW 代表尚未执行和正在执行
//所以只要不是 NEW,都算执行完了
//所以不要误认为 isDone 就是任务正常执行,还包括异常退出和任务取消
return state != NEW;
}
8.2 CompletionService
后面要讲的 ExecutorService 的实现会用到 CompletionService,但 CompletionService 实际上也会用到 Executor,所以涉及到 Executor 的部分留到后面再讲。
8.2.1 接口方法
也不复杂,共 5 个:
- submit(Callable):Future,非阻塞方法,提交一个 Callable 任务,返回 Future,用于获取结果;
- submit(Runnable, V):Future,非阻塞方法,提交一个 Runnable 任务,返回 Future,主要用于感知任务是否执行完成,因为任务返回的值是已知的,就是自己在参数里传入的;
- take():Future,阻塞方法,等待直到接下来第一个任务执行完成,取走其 Future;
- poll():Future,非阻塞方法,取走目前第一个执行完成的任务的 Future,如果没有已经执行完成的立即返回 null;
- poll(long,TimeUnit):Future,阻塞方法,取走目前第一个执行完成的任务的 Future,如果没有已经执行完成的,可以等待指定的时间。
它的核心功能主要在后面三个方法,即通过 submit 提交一批任务,然后 take 或者 poll 拿到最先完成的任务。
这样可以避免,前面执行的任务耗时很长,导致后面执行的任务已经完成了也拿不到结果。
8.2.2 demo
package per.lvjc.concurrent.futuretask;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class CompletionServiceDemo {
private static Executor executor = Executors.newFixedThreadPool(10);
private static ExecutorCompletionService<Integer> ecs = new ExecutorCompletionService<>(executor);
public static void main(String[] args) throws ExecutionException, InterruptedException {
//new 出 10 个 Callable
List<Callable<Integer>> tasks = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int finalI = i;
tasks.add(() -> {
TimeUnit.SECONDS.sleep(10 - finalI);
return finalI;
});
}
//不用 CompletionService,统计耗时
getAndProcessResultByFutureGet(tasks);
//用 CompletionService,统计耗时
//getAndProcessResultByCompletionService(tasks);
}
private static void getAndProcessResultByFutureGet(List<Callable<Integer>> tasks) throws ExecutionException, InterruptedException {
//提交所有任务
List<FutureTask<Integer>> futureTasks = new ArrayList<>();
for (Callable<Integer> task : tasks) {
FutureTask<Integer> futureTask = new FutureTask<>(task);
futureTasks.add(futureTask);
executor.execute(futureTask);
}
//统计等待所有任务执行完成,并处理完所有结果的耗时
long start = System.currentTimeMillis();
for (FutureTask<Integer> futureTask : futureTasks) {
int result = futureTask.get();
processResult(result);
}
long end = System.currentTimeMillis();
System.out.println("all result processed, cost " + (end - start) + " ms");
}
private static void getAndProcessResultByCompletionService(List<Callable<Integer>> tasks) throws InterruptedException, ExecutionException {
//提交所有任务
for (Callable<Integer> task : tasks) {
ecs.submit(task);
}
//统计等待所有任务执行完成,并处理完所有结果的耗时
long start = System.currentTimeMillis();
int taskSize = tasks.size();
int count = 0;
while (true) {
Future<Integer> future = ecs.take();
processResult(future.get());
count++;
if (count == 10) {
break;
}
}
long end = System.currentTimeMillis();
System.out.println("all result processed by ecs, cost " + (end - start) + " ms");
}
private static void processResult(int result) throws InterruptedException {
//处理每个任务的结果耗时 1 秒
TimeUnit.SECONDS.sleep(1);
System.out.println("processed result:" + result);
}
}
不用 CompletionService,执行结果如下,耗时 20 秒:
processed result:0
processed result:1
processed result:2
processed result:3
processed result:4
processed result:5
processed result:6
processed result:7
processed result:8
processed result:9
all result processed, cost 20029 ms
用 CompletionService,执行结果如下,耗时 11 秒:
processed result:9
processed result:8
processed result:7
processed result:6
processed result:5
processed result:4
processed result:3
processed result:2
processed result:1
processed result:0
all result processed by ecs, cost 11035 ms
出现这样的结果是因为,这 10 个任务,前面的耗时较长,后面的耗时较短。
虽然都是提交给线程池执行,但是不用 CompletionService 的情况,傻傻地等第一个耗时最长的任务,后面的任务已经执行完成了也不先拿出来去处理,导致总耗时:等待任务结果 10 秒 + 处理结果 10 秒 = 20 秒。
而使用了 CompletionService,耗时最短的任务结果出来了,先拿去处理,这样总耗时:等待任务结果 10 秒 + 处理结果 1 秒 = 11 秒。
8.2.3 实现
实现其实很简单,看到 take,poll 这些方法,显然是一个 BlockingQueue。
在 ExecutorCompletionService 类里面:
private class QueueingFuture extends FutureTask<Void> {
QueueingFuture(RunnableFuture<V> task) {
super(task, null);
this.task = task;
}
protected void done() { completionQueue.add(task); }
private final Future<V> task;
}
主要就这个 QueueingFuture,重写了 FutureTask 的 done 方法,任务执行完成后,把执行结果放到队列里面。