开始前,会这些就够了
- 能阅读 Java 方法调用与 try
/catch - 知道 Future 用来表示稍后才会出现的结果
- 先读 页面已经报超时,后台为什么还在干活?:先把结果状态和实际工作停止分开,再看聚合与输入的关系。
查价格和查库存各有一个结果对象。价格已经失败,页面却仍等着 allOf:先别急着判断线程死锁,观察聚合对象究竟在等什么。
本实验不发远程请求。我们调用真实 JDK 类,由主线程逐步完成各分支,让“价格失败时库存尚未完成”成为确定的前提,再核对聚合状态。
把例子带到自己的电脑上
下载完整练习包
需要 完整 JDK 17 或更新版本;实测 Windows / JDK 17.0.20.1 / 21.0.12。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- Main.java:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
java -version
javac -version
javac -encoding UTF-8 --release 17 Main.java
java -cp . Main解压后,在含 Main.java 的目录打开终端。命令分别回车执行。真实 CompletableFuture、主线程显式完成顺序、每 JDK 18 项检查;未启动工作线程,未测网络取消、压力或异常竞争选择。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
先让一个任务失败,聚合结果会立即结束吗?
正在准备实验控件…
这是两个输入 Future 的完成状态模型,不启动 Java 线程,也不模拟线程池和任务取消。真实 CompletableFuture 行为请运行下方 Java 示例;异常完成不代表业务副作用自动撤销。
先区分三个状态:分支失败、所有分支完成、聚合失败
price 和 stock 是两个结果对象;all 是 CompletableFuture.allOf(price, stock) 返回的第三个对象。一个分支失败,不意味着另外两个对象都已经完成。
这里的“完成”包含正常完成和异常完成。allOf 需要所有输入都完成;只要其中有失败,聚合最后也以异常结束。不要把“已经知道最终不会全成功”和“聚合已经结束”当成同一件事。
程序先让 price.completeExceptionally(...),随后检查 stock.isDone() 和 all.isDone();二者都应为 false。直到调用 stock.complete(...),all 才进入异常完成状态。这个先后关系直接由代码决定,不依赖某次线程调度。
allOf 是一个完成条件,不会生成业务结果清单
allOf 返回 CompletableFuture<Void>。正常结束时 join() 得到 null;它没有把每个价格、库存或异常自动装成一个 List。要读业务结果,仍要保存并查看原来的分支。
主实验只有 price 一个失败,所以捕获 CompletionException 后可以精确检查 getCause() 是否就是放进去的 priceError。多个分支同时失败时,不应把聚合抛出的那一个 cause 当成“全部错误”,也不依赖它必然代表墙上时钟最早发生的失败。
如果某个输入永久不完成,allOf 也没有理由自行结束。实际系统仍需为分支设置明确的完成和资源处理规则。本页不把超时、停止网络请求和关闭资源混成一个动作。
anyOf 也不是“等第一个成功”,取消聚合也不是停止所有工作
对照组先让 first 失败,再让 later 成功。anyOf 已在 first 失败时异常完成,later 的成功不会把它改成成功。选用 anyOf 前,先明确业务接受的是任意一个完成结果,还是要继续寻找可用结果。
另一组只取消 allOf 返回的聚合对象,然后观察 childA、childB。两个输入仍没有完成,随后还能各自正常完成;聚合仍保持取消状态。取消依赖结果不会反向取消作为输入的结果对象。
本程序没有启动计算线程。因此它证明的是 CompletableFuture 的状态关系,不是某个线程能否被中断。CompletableFuture.cancel(true) 的 true 也不能作为底层工作已经停止的凭证。
需要每一项结果,就为每个分支建立结果记录
程序给 A、B、C 分别接上 handle,将正常值或异常类型转换成教学用字符串。再对这三个记录对象调用 allOf;这样等待的是“每一项都已经形成记录”。
我们故意按 B 失败、C 成功、A 失败的顺序完成输入。最终按输入列表 A、B、C 的顺序读取记录,输出两项不同错误和一项成功。完成顺序与报告顺序可以分别设计。
handle 返回的记录成功生成,不表示原业务成功了。本例的 ERROR 字样必须保留;真实接口宜使用带状态、业务编号和错误分类的结构,不能把所有失败都转换成一个看起来正常的空值。handle 自己若抛异常,记录阶段仍会失败。
运行完整程序:两套 JDK 各通过 18 项检查
下载项目或保存下面唯一的完整 Main.java 即可运行。它使用显式断言,所以漏写 -ea 不会跳过检查。没有线程池、sleep、真实接口或第三方依赖。
2026-10-23 在 Windows 的 Microsoft OpenJDK 17.0.20.1 与 21.0.12 上分别以 --release 17 编译并运行;每版 18 项检查,stdout 统一换行后一致。程序最后的 PASS 18 是断言数,不是并发吞吐量或压力测试结果。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
public final class Main {
private static int checks;
private static void check(boolean ok, String message) {
checks++;
if (!ok) throw new AssertionError(message);
}
private static Throwable failureOf(CompletableFuture<?> future) {
try {
future.join();
throw new AssertionError("Expected failure");
} catch (CompletionException failure) {
return failure.getCause();
}
}
private static String outcome(String name, String value, Throwable error) {
return error == null ? name + ":OK:" + value
: name + ":ERROR:" + error.getClass().getSimpleName();
}
public static void main(String[] args) {
// Explicit completions make the observation order independent of a scheduler.
CompletableFuture<String> price = new CompletableFuture<>();
CompletableFuture<String> stock = new CompletableFuture<>();
CompletableFuture<Void> all = CompletableFuture.allOf(price, stock);
IllegalStateException priceError = new IllegalStateException("price unavailable");
price.completeExceptionally(priceError);
check(price.isCompletedExceptionally(), "price failed");
check(!stock.isDone(), "stock still pending");
check(!all.isDone(), "allOf waits for all inputs");
System.out.println("after-price-failure: price.failed=" + price.isCompletedExceptionally()
+ "; stock.done=" + stock.isDone() + "; all.done=" + all.isDone());
stock.complete("in-stock");
check(all.isCompletedExceptionally(), "allOf now completed exceptionally");
check(failureOf(all) == priceError, "single input failure is preserved as cause");
check(stock.join().equals("in-stock"), "successful sibling keeps its result");
System.out.println("after-stock-success: all.failed=" + all.isCompletedExceptionally()
+ "; cause=" + failureOf(all).getClass().getSimpleName()
+ "; stock=" + stock.join());
CompletableFuture<String> first = new CompletableFuture<>();
CompletableFuture<String> later = new CompletableFuture<>();
CompletableFuture<Object> any = CompletableFuture.anyOf(first, later);
first.completeExceptionally(new IllegalArgumentException("first failed"));
check(any.isCompletedExceptionally(), "anyOf accepts exceptional completion");
check(!later.isDone(), "anyOf does not finish its sibling");
later.complete("later success");
check(failureOf(any) instanceof IllegalArgumentException, "later success does not replace failure");
System.out.println("anyOf: first=ERROR; later=OK; aggregate="
+ failureOf(any).getClass().getSimpleName());
CompletableFuture<String> childA = new CompletableFuture<>();
CompletableFuture<String> childB = new CompletableFuture<>();
CompletableFuture<Void> cancelled = CompletableFuture.allOf(childA, childB);
check(cancelled.cancel(true), "aggregate cancellation succeeds");
check(!childA.isDone() && !childB.isDone(), "cancellation is not propagated to inputs");
System.out.println("cancel-aggregate: cancelled=" + cancelled.isCancelled()
+ "; childA.done=" + childA.isDone() + "; childB.done=" + childB.isDone());
childA.complete("A");
childB.complete("B");
check(cancelled.isCancelled(), "aggregate remains cancelled");
check(childA.join().equals("A") && childB.join().equals("B"), "children can still complete");
CompletableFuture<String> a = new CompletableFuture<>();
CompletableFuture<String> b = new CompletableFuture<>();
CompletableFuture<String> c = new CompletableFuture<>();
List<CompletableFuture<String>> outcomes = List.of(
a.handle((value, error) -> outcome("A", value, error)),
b.handle((value, error) -> outcome("B", value, error)),
c.handle((value, error) -> outcome("C", value, error)));
CompletableFuture<Void> collected = CompletableFuture.allOf(
outcomes.toArray(CompletableFuture[]::new));
b.completeExceptionally(new IllegalArgumentException("B failed"));
c.complete("ready");
check(!collected.isDone(), "collect waits for uncompleted A");
a.completeExceptionally(new IllegalStateException("A failed"));
check(collected.join() == null, "allOf has no result list");
List<String> report = outcomes.stream().map(CompletableFuture::join).toList();
check(report.equals(List.of("A:ERROR:IllegalStateException", "B:ERROR:IllegalArgumentException",
"C:OK:ready")), "report keeps every outcome in input order");
System.out.println("collected=" + report);
check(CompletableFuture.allOf().join() == null, "empty allOf is complete");
check(!CompletableFuture.anyOf().isDone(), "empty anyOf is incomplete");
System.out.println("empty: allOf.done=" + CompletableFuture.allOf().isDone()
+ "; anyOf.done=" + CompletableFuture.anyOf().isDone());
System.out.println("PASS " + checks);
}
}
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-23 使用 完整 JDK 17 或更新版本;实测 Windows / JDK 17.0.20.1 / 21.0.12 复跑这份源码并核对完整输出。真实 CompletableFuture、主线程显式完成顺序、每 JDK 18 项检查;未启动工作线程,未测网络取消、压力或异常竞争选择。
运行后对照结果
after-price-failure: price.failed=true; stock.done=false; all.done=false
after-stock-success: all.failed=true; cause=IllegalStateException; stock=in-stock
anyOf: first=ERROR; later=OK; aggregate=IllegalArgumentException
cancel-aggregate: cancelled=true; childA.done=false; childB.done=false
collected=[A:ERROR:IllegalStateException, B:ERROR:IllegalArgumentException, C:OK:ready]
empty: allOf.done=true; anyOf.done=false
PASS 18
读输出时,先找未完成的输入,再判断聚合
第一行最关键:price.failed=true,同时 stock.done=false、all.done=false。第二行在库存正常完成以后,才出现 all.failed=true。成功的库存结果仍保留,失败的聚合没有回滚它。
collected 一行展示三个逐项结果,不能只凭 collected.join() 没抛异常就报告“查询全部成功”。报告结构表达了失败,调用方仍需根据业务决定部分成功能否使用。
空输入也要有约定:allOf() 立刻正常完成,而 anyOf() 没有任何可以触发它的输入,保持未完成。不要对空 anyOf 直接做无期限 join;本程序只读取 isDone,不等待它。
把实验带回接口时,需要再补哪些证据
这篇在已有组合树和超时知识上,补的是失败发生后各个对象的实际状态、取消方向与逐项失败记录。它没有实现新的线程池、重试器或“首个成功”组合器。
接入真实接口时,需要另外记录请求开始、结果完成、取消请求发出和资源真正关闭的时点。仅靠 Future 的一个布尔状态无法回答全部问题。
已验证范围是 Windows、两个已列明 JDK 版本、手动受控完成的真实标准库调用。未测高并发、不同执行器、网络取消、响应时间分位数或多个失败之间的异常选择顺序。
记住这三点
allOf等待全部输入完成;其中一个失败不等于聚合立即完成。anyOf接受异常完成,后来成功不会替换已有失败。- 取消聚合不反向取消输入;要保留全部结果,逐项形成明确记录。
别把例子用过头
- 把完成理解为只包含成功。
- 把聚合的一个 cause 当作全部失败清单。
- 将 handle 生成记录的成功误报为业务成功。
- 对空
anyOf调用无期限 join。
自己试一小步
给四个分支设计一份可核对的部分成功报告,能否同时说明等待规则和失败归属?
- 先写出四个分支的编号、计划完成顺序和预期结果,不运行程序。
- 分别安排正常、异常与尚未完成状态;在每一步记录聚合与各分支的状态。
- 为每项建立保留编号和状态的记录,说明报告按输入顺序还是完成顺序展示。
- 再单独取消一个聚合对象,核对输入对象状态,并写明程序没有证明哪些底层资源行为。
做到这些,就算完成
- 预测表和真实输出分别保留。
- 每个失败有对应分支,报告没有凭空补出成功值。
- 不把聚合结束或取消写成任务线程已停止。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Java SE 17 CompletableFuture:allOf、anyOf、cancel (新标签页)↗
聚合完成条件、空输入、取消规则以及 join 的异常包装。
- Java SE 17 CompletionStage:handle 与异常完成 (新标签页)↗
每个分支可把正常或异常结果转换为明确的报告记录。
- Java SE 17 CompletionException (新标签页)↗
完成阶段抛出的异常包装与 cause 之间的关系。