FutureTask、CompletableFuture:完成状态、异常与取消
基线:Java SE 17,源码固定为 OpenJDK 17u
jdk-17.0.12+7。已核对FutureTask.java与CompletableFuture.java的完成、等待、取消和超时路径。本机实验运行 Oracle JDK 17.0.12。这里区分 Future 结果、执行线程与外部业务效果,不能把三者合并成一个“完成”标志。U09 补强completeOnTimeout固定摘录、依赖链异常传播时序实验与CF.cancel不中断的实测口径;Thread.interrupt协议、取消协作与 ThreadLocal/上下文传播移至中断协议与上下文权威讲解,本页互链不复制。
异步计算先分三种状态
调用者可能已因超时停止等待,工作线程却仍在访问数据库;Future 可能已取消,远端却已扣款;回调可能正常返回一个默认值,上游真实失败却因此被隐藏。一个可恢复异步 API 至少要明确结果状态、执行资源状态和业务状态。Future 负责观察结果,不是分布式事务,也不是强制线程终止器。
Future 契约 规定 get 等待并读取结果,成功计算的动作与之后 get 返回具有内存可见性关系;get(timeout) 限制本次等待,超时本身不取消任务。线程池 submit 经 AbstractExecutorService.newTaskFor 创建 RunnableFuture,默认是 FutureTask,再交给 execute。因此任务包装和执行器接纳是两层协议,拒绝策略仍会影响 Future 能否最终完成。
FutureTask 的七态没有 RUNNING
固定文件 FutureTask.java 中 state 是 volatile,值依次为 NEW、COMPLETING、NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTING、INTERRUPTED。运行中的 Callable 仍可能处于 NEW,执行占有由另一个 volatile 字段 runner 配合 CAS 管理;把状态机画成 NEW→RUNNING→DONE 不符合这份实现。
正常链为 run → callable.call → set → finishCompletion,异常链为 run → setException → finishCompletion,取消链为 cancel → finishCompletion;读取走 get → awaitDone → report。run 先检查 NEW 并 CAS 安装 runner,避免两个调用者同时执行同一任务;安装后再次检查 state,防止取消已经抢先取得终态。
原码:七个状态与四条迁移
原码连续摘录(仅统一公共缩进):FutureTask.java,jdk-17.0.12+7,L86–L99。
* Possible state transitions:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
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;注释直接否定了“运行态”这一常见错误:NEW 同时覆盖未开始和计算中。COMPLETING、INTERRUPTING 是写入结果或中断交接的过渡态,isDone 以 state != NEW 判断,因此可能在这两个瞬态就返回 true。
完成发布:outcome 为什么不加 volatile
正常结果不是直接把 state 从 NEW 改 NORMAL。set 先 CAS 到 COMPLETING,写普通字段 outcome,再用 release 写发布 NORMAL。setException 用同样协议发布 Throwable 到 EXCEPTIONAL。get 读取 state 并等待越过 COMPLETING 之后才能按 outcome 解释结果;这正是为什么 outcome 自身不必声明 volatile。
原码:set 的 COMPLETING 与 release 发布
原码连续摘录(仅统一公共缩进,截取方法):FutureTask.java,jdk-17.0.12+7,L228–L234。
protected void set(V v) {
if (STATE.compareAndSet(this, NEW, COMPLETING)) {
outcome = v;
STATE.setRelease(this, NORMAL); // final state
finishCompletion();
}
}CAS 取得完成权后才写结果;setRelease 把 outcome 的普通写发布给读到 NORMAL 的线程。STATE.compareAndSet 与 STATE.setRelease 是同一个原生字段上的状态转换,不能把两句拆开当成独立的同步工具。
教学改写如下,省略钩子、等待者管理和部分边界:
执行:
若不是NEW或未抢到runner:返回
再确认NEW,然后调用用户计算
计算成功:尝试CAS NEW -> COMPLETING,写结果,发布NORMAL
计算异常:尝试CAS NEW -> COMPLETING,写异常,发布EXCEPTIONAL
finally清runner;若正在中断交接,等待其稳定COMPLETING 是结果写入中的过渡状态。isDone 使用 state 不等于 NEW 判断,因此可能在结果尚未完整发布的极短窗口已经为 true;get 仍会等到可报告状态。不能通过 isDone 后直接反射读取 outcome 代替 get,也不能把内部瞬态观察包装成稳定 API。
取消:结果终态与中断计算分开
cancel(false) 若 CAS 赢得 NEW,就发布 CANCELLED,即使用户计算已经开始,只要尚未把状态推进到 COMPLETING,它仍可能取消结果。该调用不会发中断,计算可以继续并尝试 set,但这时 set 的 CAS 失败,计算结果被丢弃。于是“cancel(false) 只可能取消还没运行的任务”是错误的;它取消的是这个 Future 的结果协议。
cancel(true) 先把 NEW 改 INTERRUPTING,若 runner 非空则调用 interrupt,finally 发布 INTERRUPTED 并通知等待者。INTERRUPTING 把发中断与 runner 收尾连接起来;run 在清 runner 后复查状态,必要时等待中断交接结束,防止迟到的取消中断穿越任务边界。该实现不会无条件清除所有中断,因为中断也可能是任务自行使用的通信协议。
原码:cancel 的 INTERRUPTING 交接
原码连续摘录(仅统一公共缩进,截取方法):FutureTask.java,jdk-17.0.12+7,L164–L182。
public boolean cancel(boolean mayInterruptIfRunning) {
if (!(state == NEW && STATE.compareAndSet
(this, 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
STATE.setRelease(this, INTERRUPTED);
}
}
} finally {
finishCompletion();
}
return true;
}首先 CAS 竞争 NEW,失败说明别的完成路径先推进,不能覆盖。cancel(false) 不执行 interrupt;cancel(true) 经过 INTERRUPTING,尝试中断后写入 INTERRUPTED。finally 中的 finishCompletion 整理等待者;整段没有回滚业务副作用的操作。
若 set 先赢得 NEW→COMPLETING,cancel 失败;若 cancel 先赢,后续正常/异常结果无法覆盖取消终态。取消返回 true 不意味着 Callable 已经退出,更不意味着已完成的 SQL、消息发送或付款被撤销。线程需要在循环、阻塞 API 和资源清理处合作响应中断;吞掉 InterruptedException 后继续循环会使“已取消但仍占线程”成为常态。
等待者栈与取消竞争
awaitDone 用 WaitNode 组成等待者栈,park 前后循环检查 state,处理中断和超时;finishCompletion 取出等待者并逐个 unpark,再调用 done 并清理 Callable 引用。等待者超时或被中断时需要移除/清理节点,避免失效线程引用长期滞留。这里不是一个 FIFO 任务执行队列,等待顺序也不决定任务执行顺序。
report 对 NORMAL 返回值,对取消态抛 CancellationException,对 EXCEPTIONAL 包装为 ExecutionException。不要只 catch ExecutionException:取消是另一种结果。等待 get 时被中断通常意味着等待者取消等待,不自动转成被等待任务的 cancel;调用方是否还要取消任务,应由资源所有权和共享使用关系决定。
CompletableFuture:结果槽与完成图
CompletableFuture.java 使用 volatile result,null 表示未完成;正常 null 值使用 NIL 哨兵,异常通过 AltResult 保存 Throwable。result 不是 FutureTask 的七态整数,CompletableFuture 也不保存一个可统一代表底层计算的 runner。
complete/completeExceptionally/cancel 竞争同一个完成槽,常规完成只能由一个竞争者赢得。完成后 postComplete 传播依赖节点;thenApply 等阶段使用相应 Completion 节点,把源、目标和函数连接起来。主链可读 thenApply → uniApplyStage → UniApply.tryFire → completeValue/completeThrowable → postFire。这里是完成图,不是每个 then 都新建线程的固定流水线。
按 UniApply 关键分支精简的教学伪代码:
源尚未完成或节点已被清理 -> 暂不推进
目标已完成 -> 不再执行业务函数
源为异常 -> 将异常传播到目标
需要执行器且尚未取得执行责任 -> 提交节点,当前返回
否则调用用户函数:
成功 -> 完成目标
抛错 -> 异常完成目标
最后清理连接并继续传播thenApply 把 T 映射成 U;thenCompose 用于函数已经返回 CompletionStage 的依赖,避免 Future 套 Future;thenCombine 等待两个独立来源并组合,不负责撤销其中任何一个来源的外部效果。allOf 返回 Void 阶段,等待所有输入完成后再报告整体结果,不收集类型化结果,也不等同遇到第一个错误就停止所有任务。anyOf 取某个最先完成的结果,最先完成可能是失败,不等于最先成功。
执行线程、共享池与饥饿
不带 Async 的阶段可能由完成源阶段的线程执行,也可能在注册时发现源已经完成而由当前调用线程直接执行。因此在 thenApply 里进行阻塞 I/O,可能把完成回调线程或请求线程长时间占住。带 Async 只是安排到执行器,仍需明确哪个执行器及其拒绝语义。
该标签默认 ASYNC_POOL 在 commonPool 并行度大于 1 时使用 ForkJoinPool.commonPool,否则回退为每任务新线程。不能把默认执行器写成无条件 commonPool。生产中应显式传入有界、可观测的执行器,区分 CPU 计算、阻塞 I/O 与短回调;“使用异步 API”不自动限制并发,也不保证网络客户端释放连接。CompletableFuture API 执行策略 与源码 USE_COMMON_POOL、screenExecutor 可以交叉核对。
一个固定两线程池中,两个父任务各 submit 子任务到同池并 join,可能占满所有执行资源而等待排队子任务。这类依赖饥饿不需要监视器死锁。优先使用 thenCompose 表达依赖,避免在工作线程里阻塞等待同池任务;拆池也必须检查反向依赖和总在途上限,不能单纯多建几个池。
异常处理到底改变了哪个节点
exceptionally 在源异常时提供替代值,成功返回后,下游可能看到正常结果;handle 对成功与失败都映射新结果;whenComplete 主要观察,不为原异常提供替代成功值,但观察函数自身抛错也会影响返回阶段。必须保留并观察这些方法返回的新阶段,不能注册了处理器却仍只 get 旧引用,然后认为恢复逻辑不生效。
传播时序可以逐节点读出(PoolAsyncProbe 第 5 节实测):源以 src-fail 异常完成后,直接挂在源上的 thenApply 阶段以同一个 cause 异常完成(join 抛 CompletionException(src-fail)),业务函数从未执行;exceptionally(e -> 0) 之后接 thenApply(x -> x + 1) 得到正常值 1——恢复节点是分界线,它之后的下游收到正常值,它之前的节点保持同因失败;whenComplete 的观察器能拿到原 cause,但它返回的阶段仍异常完成(观察不等于恢复);handle((v, e) -> -1) 把失败映射成正常 -1。所以“异常传播到哪里停住”取决于链上第一个恢复类节点的位置,而不是异常类型。用默认值表示“查询失败”会与真实空结果混淆,恢复类节点应选择明确的结果类型、错误标记与降级指标,不能只靠一个魔法默认值。
allOf 的完成条件是“全部完成”,不是“全部成功”:一快一慢两个源,快的失败、慢的成功,allOf(...).join() 抛出快源的异常,但此时慢源 isDone() 为真——失败已经发生,整体仍等全部收尾。anyOf 被“最先完成者”赢得,而最先完成可以是失败(实验里已完成的失败源赢过 150ms 后才成功的源)。两者都不会取消输家:占用的线程、连接与远端副作用要由调用方按资源所有权清理。
get 采用 Future 风格的 ExecutionException,join 通常抛未检查的 CompletionException;取消时本阶段可直接抛 CancellationException,依赖阶段可能以 CompletionException 包装取消原因。业务异常应沿 cause 定位,不要对一切 CompletionException 无条件重试;非法参数与暂时连接故障具有不同重试安全性。
超时是完成策略,取消是另一条协议
orTimeout 与 completeOnTimeout 都返回当前 CompletableFuture 并安排延时完成:前者异常完成为 TimeoutException,后者正常完成为默认值。把它们当作“创建一个互不影响的超时视图”会影响其他共享调用者。若同一个源被多个调用者复用,需要先构造独立依赖视图,并决定谁拥有底层任务取消权。
该标签 orTimeout 经 Delayer 调度 Timeout,竞争源的 result;正常完成后 Canceller 负责取消不再需要的延时任务。若超时先赢,后来底层计算仍可能运行,尝试完成会失败。completeOnTimeout 则把“来不及”变成正常值,要额外记录降级,否则错误率看似下降而真实服务能力已退化。
原码:completeOnTimeout 的默认值完成
原码连续摘录(仅统一公共缩进,截取方法局部):CompletableFuture.java,jdk-17.0.12+7,L2724–L2733。
public CompletableFuture<T> completeOnTimeout(T value, long timeout,
TimeUnit unit) {
if (unit == null)
throw new NullPointerException();
if (result == null)
whenComplete(new Canceller(Delayer.delay(
new DelayedCompleter<T>(this, value),
timeout, unit)));
return this;
}与 orTimeout 同构:result 为空才登记延时任务,DelayedCompleter 到点用 value 尝试 complete,同样 return this。差别只在终态类型——默认值是正常完成。PoolAsyncProbe 第 6 节实测对照:orTimeout(150ms) 的源在超时后 get 抛 ExecutionException(TimeoutException),释放闩锁后底层计算继续、效果计数到 2,但迟到的真实结果无法覆盖超时终态;completeOnTimeout("fallback",150ms) 的源 get 返回 "fallback",释放后真实值 "real" 同样被丢弃。两者的共同点是“完成策略”而非“执行控制”:都不中断、不撤销底层计算(中断协议)。
还有一个容易被忽略的边界:supplyAsync 提交的 AsyncSupply 任务在真正调用 supplier 前会检查 result == null——如果取消或超时在任务开始前就完成了结果槽,supplier 会被整个跳过(连接都不会发起)。实验为此用“已启动闩锁”钉住交错:结论“取消不中断已运行的计算”只对已启动的任务成立;未启动的任务可能根本不执行,这也是“没运行”与“运行了但结果被丢弃”要分开观测的原因。
CompletableFuture.cancel 的 mayInterruptIfRunning 在此实现中不起控制线程作用;它把结果槽改成 CancellationException,再传播完成。与 FutureTask.cancel(true) 不同,不能期待它找到线程并 interrupt。官方 cancel 说明 明确这一点。PoolAsyncProbe 第 8 节实测:已启动并阻塞在闩锁上的 supplier 在 cancel(true) 后继续等待(效果计数保持 0),释放后才执行并完成计数——工作线程从未收到中断。anyOf 选出赢家后也不会自动取消输家;取消结果阶段更不会自动取消整个依赖图。
更完整的做法是保存实际任务句柄、共享一个截止时间和协作取消信号;每次排队、连接获取、请求发送和重试都使用剩余预算。即使取消传到了网络客户端,已发送请求也可能已在远端提交,因此还要业务幂等与结果查询。关闭资源、撤销预订与补偿付款应分别定义,不能统一叫“取消 Future”。
原码:orTimeout 返回的还是同一对象
原码连续摘录(仅统一公共缩进,截取方法):CompletableFuture.java,jdk-17.0.12+7,L2703–L2710。
public CompletableFuture<T> orTimeout(long timeout, TimeUnit unit) {
if (unit == null)
throw new NullPointerException();
if (result == null)
whenComplete(new Canceller(Delayer.delay(new Timeout(this),
timeout, unit)));
return this;
}result 为空才登记超时任务,并安装完成回调清理计时器;return this 说明不是隔离副本,其他观察者也会看到同一节点终态。这里没有业务线程 interrupt,与 FutureTask 的中断分支不同,更不能据此推断远端提交被撤销。
ThreadLocal 在异步链中的资源所有权
ThreadLocal 值属于工作线程,不属于 CompletableFuture。请求线程设置的事务、身份或 MDC 不会因为 thenApplyAsync 自动正确继承;InheritableThreadLocal 在线程创建时传递,不能表达线程池每次任务提交的上下文。池复用还可能使前一请求未清理的值被后一请求看见。弱键强值 Entry、泄漏观测、remove 责任、capture-restore 装饰器与 interrupt 协作的完整权威讲解在中断协议与上下文:任务入口捕获必要且不可变的上下文,执行前安装,finally 恢复/移除,才是明确协议;不要复制活跃事务连接或可变安全对象到任意异步线程。
实验:取消、超时与恢复
PoolAsyncProbe 的 poolasync 模式(U09 起本页实验主入口)覆盖依赖链传播、两种超时策略与三类取消协作,从根目录运行:
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home \
bash labs/java-senior/run.sh poolasync与本页直接相关的三节本轮实际输出(节选):
== 5. CompletableFuture chain: exception propagation and recovery ==
source fails -> thenApply downstream join throws CompletionException(same cause)
exceptionally(0).thenApply(+1) -> 1
whenComplete observes cause but its stage stays failed; handle -> -1
allOf(failed, slow-success): join throws, but slow stage IS done
anyOf: first completed was the failure -> join throws
== 6. orTimeout vs completeOnTimeout ==
orTimeout(150ms): get threw TimeoutException; after release effects=2
completeOnTimeout("fallback",150ms): get -> "fallback"; late "real" cannot overwrite
== 8. cancel cooperation: blind vs cooperative task, CF.cancel ==
cancel(true) on interrupt-blind task: thread still alive 250ms later; computation finished (effects=1); get -> CancellationException
cancel(true) on cooperative task: InterruptedException observed, exited promptly, effects=0
CF.cancel(true): stage cancelled, worker NOT interrupted (effects stayed 0 until release), supplier still ran第 5 节逐节点断言传播时序(同因传播、恢复分界、观察保留失败、allOf 等全部、anyOf 首完成可为失败)。第 6 节用“已启动闩锁”钉住 supplier 运行后再触发超时,对照两种终态并验证迟到结果不回滚。第 8 节三类取消:不检查中断的 FutureTask 计算在 cancel(true) 后照常完成(结果槽已归取消);协作任务经 InterruptedException 即刻退出;CF.cancel(true) 阶段取消但工作线程从未收到中断、释放后 supplier 照常执行。
历史断言保留在 async 模式(SeniorProbe),与本页口径一致:
PASS FutureTask cancel(false): cancelled result, continuing computation
PASS CompletableFuture timeout completes result, not underlying side effect输入一:FutureTask 的 Callable 用闩锁确认开始后等待释放;此时调用 cancel(false),检查 isCancelled,再释放 Callable 并记录它实际已执行的计数。预期结果已取消但计算仍能完成一次,get 抛 CancellationException。结束后有界 join,防止失败路径遗留线程。
输入二:CompletableFuture.supplyAsync 在独立线程中等闩锁,源使用 orTimeout 并用有时限 get 等待异常,确认超时后再释放计算。预期 Future 异常完成,而后台效果计数在释放后仍增长。该实验不需要用“正好睡 10ms”推测线程已开始,步骤必须由闩锁协调。
输入三:失败源分别接 exceptionally、handle、whenComplete;检查前两者返回的阶段可成为正常值,观察阶段保留失败;另用已完成源与具名执行器比较同步、异步阶段线程身份。线程名称只解释本次实验,不能把某个具体线程固定为 API 保证。
纸面实验令 T1 执行 set、T2 执行 cancel(true)。逐步排列谁先 CAS 离开 NEW,写出最后 state、outcome 是否可返回、runner 是否收到中断,以及外部写入是否可能已经发生。验收必须区分“结果不再可见”和“动作未执行”,不能只画终态箭头。
完成时间线:set 与 cancel 谁先赢
| 时间 | T1 执行 set | T2 执行 cancel(true) | 最终 state | 结果与副作用 |
|---|---|---|---|---|
| t1 | CAS NEW→COMPLETING 成功 | — | COMPLETING | 计算已产生结果 |
| t2 | 写 outcome,发布 NORMAL | CAS 失败 | NORMAL | get 返回结果,取消返回 false |
| t3 | — | — | — | 外部写入可能已发生 |
| 时间 | T1 执行 set | T2 执行 cancel(true) | 最终 state | 结果与副作用 |
|---|---|---|---|---|
| t1 | 尚未 CAS | CAS NEW→INTERRUPTING 成功 | INTERRUPTING | 取消已赢得结果槽 |
| t2 | CAS 失败,结果被丢弃 | interrupt runner,发布 INTERRUPTED | INTERRUPTED | get 抛 CancellationException |
| t3 | 计算可能继续 | — | — | SQL/付款是否发生由任务决定 |
两张表说明:取消成功只保证结果槽终态,不保证计算停止;set 成功也不保证之前的远端副作用未发生。
架构师变化条件:聚合、尾延迟和取消成本
假设接口 deadline 为 300ms,前置鉴权 20ms,响应处理预留 30ms,剩余 250ms 供三个并行依赖使用。三分支各自设置 250ms 并不意味着可以再给重试 250ms;重试应扣除已花时间和排队时间。并行不把下游容量变大,到达率 100/s、每请求三个 RPC,基础调用流量已是 300/s,重试还会放大。
条件改为任何两路成功即可返回,allOf 不符合此完成条件,需要显式计数/聚合状态,并定义失败、取消、最后不足两路和迟到结果的处理。条件改为最先成功,anyOf 也不直接满足,因为首个失败可能先触发;应构造只在成功时计数、全失败时确定异常的组合。
条件改为两路结果必须属于同一配置版本,要在调用和组合时携带版本并验证,不能用“都正常完成”代替一致性。条件改为写操作则更严格:客户端超时后保留业务请求 ID、查状态、对账,取消只用于压缩资源占用,业务恢复另有责任人和日志。
指标至少包含阶段耗时、执行器队列年龄、超时后仍运行任务数、外部请求取消率、降级比例与错误原因。平均响应变快而后台未完成任务持续增加,可能只是把等待藏到了返回之后,必须验证稳态资源与恢复能力。
三轮递进追问
第一轮:FutureTask 有 RUNNING 态吗,结果如何安全发布?没有,runner 与 NEW 分工,CAS 取得完成权,写 outcome 后 release 发布终态。评分点:内部状态、发布顺序、读端等待。
第二轮:CF 超时后为何仍有 SQL?超时竞争结果槽,没有强制控制所有计算;CF 取消也不发执行线程中断。评分点:与 FutureTask 区别、任务句柄与资源清理。
第三轮:聚合接口取消输家能保证付款没发生吗?不能,远端提交可能早于确认;需要业务身份、查单和幂等,完成图只描述局部结果依赖。评分点:业务终态、端到端预算、迟到结果。
闭卷验收
本页 13 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。
来源与版本
| 来源 | 版本 | 用途 |
|---|---|---|
| Java SE 17 Future API | Java SE 17 | get、超时、取消与内存可见性 |
| Java SE 17 CompletableFuture API | Java SE 17 | 完成图、异常传播、orTimeout、cancel 与默认执行器 |
| Java SE 17 ExecutorService API | Java SE 17 | submit、invokeAll、关闭与终止 |
| JEP 266: More Concurrency Utilities | Java 8/9 | CompletableFuture 与可完成/超时能力(orTimeout/completeOnTimeout 自 9 起)的设计来源 |
| OpenJDK 17u FutureTask | jdk-17.0.12+7 | 七态(L86–L99)、cancel(L164–L182)、set(L228–L234) |
| OpenJDK 17u CompletableFuture | jdk-17.0.12+7 | result 完成槽、UniApply、orTimeout(L2703–L2710)、completeOnTimeout(L2724–L2733) |
本地 PoolAsyncProbe poolasync | Oracle JDK 17.0.12 | 依赖链传播时序、orTimeout/completeOnTimeout 对照、三类取消协作 |
本地 SeniorProbe async | Oracle JDK 17.0.12 | cancel(false) 继续计算、orTimeout 不停止副作用(历史断言) |
访问日期:2026-10-25(U09 复核;初版 2026-10-23)。完成槽、UniApply 节点和 Delayer 属于固定版本实现,不是 API 契约;升级 JDK 需重新核对。