qinghuan
← 回到知识探索

后端现场 / 可下载小项目

一个分支早就失败,allOf 为什么还没有结束?

手动推进三个真实 CompletableFuture,区分等待全部、任意完成、取消聚合,以及保留每项失败。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 先区分三个状态:分支失败、所有分支完成、聚合失败
  4. allOf 是一个完成条件,不会生成业务结果清单
  5. anyOf 也不是“等第一个成功”,取消聚合也不是停止所有工作
  6. 需要每一项结果,就为每个分支建立结果记录
  7. 运行完整程序:两套 JDK 各通过 18 项检查
  8. 读输出时,先找未完成的输入,再判断聚合
  9. 把实验带回接口时,需要再补哪些证据
  10. 记住这三点
  11. 别把例子用过头
  12. 自己试一小步
  13. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 2 个解释
聚合完成
等一组结果对象都结束后,第三个结果对象才结束。
异常完成
结果对象已经结束,但保存的是失败而不是正常值。
打开完整术语小抄 ↗

查价格和查库存各有一个结果对象。价格已经失败,页面却仍等着 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 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

先让一个任务失败,聚合结果会立即结束吗?

正在准备实验控件…

先点“A 失败”,再观察 B 和 allOf。最后让 B 成功,比较“结果已经知道会失败”和“聚合对象已经完成”。

输入 A
等待中
输入 B
等待中
allOf 聚合
等待中

    这是两个输入 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 是断言数,不是并发吞吐量或压力测试结果。

    展开收起代码 · 小实验java · 90 行

    长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

    小实验 · java
    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。

    自己试一小步

    给四个分支设计一份可核对的部分成功报告,能否同时说明等待规则和失败归属?

    1. 先写出四个分支的编号、计划完成顺序和预期结果,不运行程序。
    2. 分别安排正常、异常与尚未完成状态;在每一步记录聚合与各分支的状态。
    3. 为每项建立保留编号和状态的记录,说明报告按输入顺序还是完成顺序展示。
    4. 再单独取消一个聚合对象,核对输入对象状态,并写明程序没有证明哪些底层资源行为。

    做到这些,就算完成

    • 预测表和真实输出分别保留。
    • 每个失败有对应分支,报告没有凭空补出成功值。
    • 不把聚合结束或取消写成任务线程已停止。

    查原始资料

    想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。