接上一篇《Java并发系列(9)——并发工具类》
8 FutureTask 与 CompletionService
把 FutureTask 和 CompletionService 放在一起,并不是因为它们之间有什么特别的联系,虽然确实有一点联系。
主要是因为下一章要讲线程池,如果不把 FutureTask 和 CompletionService 搞清楚,线程池的部分代码看得会比较困惑。
8.1 FutureTask
FutureTask 的实现比较简单,但它是一个非常基础的东西。只要涉及到有返回值的异步调用,或直接或间接一般都会用到它。
用法很简单,这里给个示例:
package 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 run 方法异常退出分支,走 setException 方法:
//处理 run 方法异常退出
run 方法成功执行分支,走 set 方法:
protected 处理可能遗漏的 interrupt:
private 如果状态是 INTERRUPTING,说明调用 cancel 方法的线程还没有结束,在这里要等它跑完。
这里是一个 while 循环,直到状态被改掉为止,通过 yield 试图把 cpu 让给其它线程,也是希望 cancel 线程能早点拿到 cpu 资源早点跑完。
最后,不管是 setException 还是 set 方法在赋值给 outcome 之后都会调用:
private 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 阻塞逻辑:
private 退出阻塞之后,获取执行结果的逻辑:
private get 方法总结:
- 用链表保存了所有在 get 方法阻塞的线程;
- 链表采用头插法,后阻塞的线程排在前面,不算超时自己醒来的的线程,会先被唤醒;
- get 会得到三种结果:
- 任务正常完成:得到任务返回值;
- 任务异常退出:抛出 ExecutionException,里面包装了任务抛出的异常;
- 任务被取消:抛出 CancellationException。
8.1.3.4 cancel 方法
主干逻辑:
//取消任务,成功取消返回 true,否则返回 false
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 8.1.3.7 isDone 方法
public 8.2 CompletionService
后面要讲的 ExecutorService 的实现会用到 CompletionService,但 CompletionService 实际上也会用到 Executor,所以涉及到 Executor 的部分留到后面再讲。
8.2.1 接口方法
也不复杂,共 5 个:
- submit(Callable<V>):Future<V>,非阻塞方法,提交一个 Callable 任务,返回 Future,用于获取结果;
- submit(Runnable, V):Future<V>,非阻塞方法,提交一个 Runnable 任务,返回 Future,主要用于感知任务是否执行完成,因为任务返回的值是已知的,就是自己在参数里传入的;
- take():Future<V>,阻塞方法,等待直到接下来第一个任务执行完成,取走其 Future;
- poll():Future<V>,非阻塞方法,取走目前第一个执行完成的任务的 Future,如果没有已经执行完成的立即返回 null;
- poll(long,TimeUnit):Future<V>,阻塞方法,取走目前第一个执行完成的任务的 Future,如果没有已经执行完成的,可以等待指定的时间。
它的核心功能主要在后面三个方法,即通过 submit 提交一批任务,然后 take 或者 poll 拿到最先完成的任务。
这样可以避免,前面执行的任务耗时很长,导致后面执行的任务已经完成了也拿不到结果。
8.2.2 demo
package 不用 CompletionService,执行结果如下,耗时 20 秒:
processed 用 CompletionService,执行结果如下,耗时 11 秒:
processed 出现这样的结果是因为,这 10 个任务,前面的耗时较长,后面的耗时较短。
虽然都是提交给线程池执行,但是不用 CompletionService 的情况,傻傻地等第一个耗时最长的任务,后面的任务已经执行完成了也不先拿出来去处理,导致总耗时:等待任务结果 10 秒 + 处理结果 10 秒 = 20 秒。
而使用了 CompletionService,耗时最短的任务结果出来了,先拿去处理,这样总耗时:等待任务结果 10 秒 + 处理结果 1 秒 = 11 秒。
8.2.3 实现
实现其实很简单,看到 take,poll 这些方法,显然是一个 BlockingQueue。
在 ExecutorCompletionService 类里面:
private 主要就这个 QueueingFuture,重写了 FutureTask 的 done 方法,任务执行完成后,把执行结果放到队列里面。
Java并发系列(10)--FutureTask 和 CompletionServiceblog.csdn.net

本文详细介绍了Java并发工具类FutureTask和CompletionService的原理与使用。FutureTask作为有返回值的异步调用基础,涉及任务的执行、取消和结果获取。CompletionService则提供了一种获取最先完成任务结果的方式,有效避免长任务阻塞短任务的处理。通过示例展示了CompletionService如何提高任务处理效率。
1641

被折叠的 条评论
为什么被折叠?



