Java 基础体系 · 第 66/100 篇。示例统一以 Java 25 LTS 为语言和 JVM 基线;框架示例使用与其兼容的现代稳定版本。

Java CompletableFuture:组合、线程池、超时、取消和异常传播

CompletableFuture 同时实现了 Future<T>CompletionStage<T>。前者表示“某个异步计算最终可以取得一个结果”,后者表示“可以在结果完成后继续编排动作”。

这两个抽象解决的问题不同:

  • Future 主要解决“提交任务、等待结果、取消任务”;
  • CompletionStage 主要解决“任务之间如何组合、转换、分支和传播异常”。

CompletableFuture<T> 的核心不是“创建线程”,而是维护一个可完成的结果状态,并在状态完成后触发依赖它的阶段。


1. 一个 CompletableFuture 的状态模型

对类型为 TCompletableFuture<T>,最终状态可以抽象为三类:

未完成
 ├── 正常完成:结果为 T
 ├── 异常完成:原因是 Throwable
 └── 取消完成:原因通常表现为 CancellationException

正常完成可以通过以下方式发生:

CompletableFuture<String> future = new CompletableFuture<>();

future.complete("ok");

异常完成:

future.completeExceptionally(new IOException("read failed"));

取消:

future.cancel(false);

一个 CompletableFuture 只能完成一次。多个线程同时调用 completecompleteExceptionallycancel 时,只有第一个成功改变状态的操作生效,其他操作返回 false 或不改变结果。

CompletableFuture<String> future = new CompletableFuture<>();

boolean first = future.complete("A"); // true
boolean second = future.complete("B"); // false

System.out.println(future.join());    // A

这是一种“竞争完成”模型:多个来源可以竞争交付结果,但消费者只能观察到一个最终状态。

1.1 isDone 不代表成功

future.isDone();       // 正常、异常、取消都返回 true
future.isCompletedExceptionally(); // 异常或取消时为 true
future.isCancelled(); // 通过 cancel 完成时为 true

因此,isDone() 只能说明“不会再改变”,不能说明“结果可用”。


2. getjoin 与异常包装

Future.get() 是可中断、可检查超时的方法:

try {
    String value = future.get(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} catch (ExecutionException e) {
    Throwable cause = e.getCause();
} catch (TimeoutException e) {
    // 等待超时
}

CompletableFuture.join() 不要求处理受检异常:

String value = future.join();

但失败时会抛出:

  • 正常异常通常包装为 CompletionException
  • 取消通常表现为 CancellationException
  • get() 则通常包装为 ExecutionException

例如:

CompletableFuture<String> failed =
        CompletableFuture.failedFuture(new IOException("network error"));

try {
    failed.join();
} catch (CompletionException e) {
    System.out.println(e.getCause()); // IOException: network error
}

join() 的便利性不能消除异常处理责任。它适合已经处于异步流程末端的代码;在需要响应中断或区分等待超时的代码中,应使用 get 或带超时的 get


3. 同步依赖动作与异步依赖动作

CompletionStage 的方法通常有三种形式:

thenApply(...)
thenApplyAsync(...)
thenApplyAsync(..., executor)

thenApply 为例:

CompletableFuture<Integer> source =
        CompletableFuture.completedFuture(10);

CompletableFuture<String> result =
        source.thenApply(value -> "value=" + value);

3.1 thenApply:转换结果,不强制切换线程

thenApply 表示:

source 完成为 T
    └── 使用 Function<T, U>
            └── 新阶段完成为 U

如果源阶段尚未完成,动作通常由完成源阶段的线程执行;如果源阶段已经完成,注册动作的线程可能直接执行它。

因此,下面的线程名不能由 API 契约固定推断:

CompletableFuture<String> result =
        CompletableFuture.supplyAsync(() -> {
            System.out.println("load: " + Thread.currentThread());
            return "data";
        }).thenApply(data -> {
            System.out.println("transform: " + Thread.currentThread());
            return data.toUpperCase();
        });

线程可能相同,也可能不同,取决于动作注册时机和哪个线程触发完成。

thenApply 适合轻量、非阻塞的转换,例如解析、映射、字段提取。它不是“在当前调用线程同步执行”的严格承诺。

3.2 thenApplyAsync:请求异步执行

CompletableFuture<String> result =
        CompletableFuture.supplyAsync(() -> load())
                .thenApplyAsync(data -> transform(data));

不带 Executor 时,使用该实现的默认异步执行器。对 CompletableFuture 的常见实现而言,通常是 ForkJoinPool.commonPool();具体默认设施属于实现行为,应避免把公共线程池当作无限容量的业务线程池。

显式指定线程池更容易表达资源边界:

ExecutorService ioPool = Executors.newFixedThreadPool(16);

CompletableFuture<String> result =
        CompletableFuture
                .supplyAsync(() -> loadFromDatabase(), ioPool)
                .thenApplyAsync(this::parse, ioPool);

这里需要区分两件事:

  1. supplyAsync(..., ioPool) 决定初始任务提交到哪里;
  2. thenApplyAsync(..., ioPool) 决定这个后续动作提交到哪里。

初始任务使用自定义线程池,并不会自动让所有后续阶段也使用该线程池。

3.3 不要用 thenApply 隐藏阻塞操作

下面代码虽然能运行,但可能占用公共线程池工作线程:

CompletableFuture<String> result =
        CompletableFuture.supplyAsync(() -> "id")
                .thenApply(id -> blockingDatabaseQuery(id));

如果 blockingDatabaseQuery 会等待网络、数据库或文件系统,应明确使用适合阻塞任务的执行器:

CompletableFuture<String> result =
        CompletableFuture.supplyAsync(() -> "id", ioPool)
                .thenApplyAsync(this::blockingDatabaseQuery, ioPool);

这不是因为 thenApply 一定错误,而是因为执行线程由完成时机决定,阻塞动作的资源归属不再清晰。


4. thenApplythenCompose:映射和扁平化

假设有一个异步函数:

CompletableFuture<User> loadUser(long id) {
    return ...
}

如果使用 thenApply

CompletableFuture<CompletableFuture<User>> nested =
        CompletableFuture.completedFuture(42L)
                .thenApply(this::loadUser);

结果类型是:

CompletableFuture<CompletableFuture<User>>

外层阶段完成,只说明“已经创建出了内层阶段”,不说明用户已经加载完成。

thenCompose 用于扁平化:

CompletableFuture<User> user =
        CompletableFuture.completedFuture(42L)
                .thenCompose(this::loadUser);

形式上,若:

f: T -> U

则:

thenApply(f): CompletionStage<T> -> CompletionStage<U>

若异步函数本身返回阶段:

g: T -> CompletionStage<U>

则:

thenCompose(g): CompletionStage<T> -> CompletionStage<U>

thenCompose 的完成条件是两个条件同时满足:

  1. 外层阶段正常完成并得到 T
  2. g(T) 返回的内层阶段完成并得到 U

如果外层阶段失败,g 不会调用;如果 g 抛异常,组合阶段失败;如果内层阶段失败,组合阶段也失败。


5. thenCombineallOfanyOf

5.1 thenCombine:两个独立结果合并

CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> loadUser(7), ioPool);

CompletableFuture<Permission> permissionFuture =
        CompletableFuture.supplyAsync(() -> loadPermission(7), ioPool);

CompletableFuture<View> viewFuture =
        userFuture.thenCombine(
                permissionFuture,
                View::new
        );

thenCombine 不会把第二个任务排在第一个任务之后。两个源阶段可以并行开始,合并函数只有在两者都正常完成后才执行。

设两个阶段的完成时间分别为 t1t2,不考虑调度和合并开销,则合并阶段最早在:

max(t1, t2)

之后完成,而不是:

t1 + t2

这就是并行分支与串行 thenCompose 的区别。

5.2 allOf:等待全部完成,但不收集结果

CompletableFuture<Void> all =
        CompletableFuture.allOf(userFuture, permissionFuture);

CompletableFuture<List<Object>> values =
        all.thenApply(ignored -> List.of(
                userFuture.join(),
                permissionFuture.join()
        ));

allOf 的返回类型是 CompletableFuture<Void>,它只提供“全部完成”的屏障,不自动生成结果列表。

调用 join() 是安全的,原因是 all 正常完成的前提是所有输入阶段都正常完成。如果其中一个失败,all 也会异常完成,后续 thenApply 不会执行。

不过,allOf 的失败信息是聚合后的单个异常,不会自动提供所有失败原因。需要完整收集每个分支结果时,应为每个分支单独处理异常。

5.3 anyOf:第一个完成者决定结果

CompletableFuture<Object> first =
        CompletableFuture.anyOf(
                CompletableFuture.supplyAsync(() -> queryReplica("A")),
                CompletableFuture.supplyAsync(() -> queryReplica("B"))
        );

anyOf 在任意一个输入阶段完成时完成:

  • 第一个正常完成:anyOf 正常完成;
  • 第一个异常完成:anyOf 异常完成;
  • 结果类型是 Object,因为输入阶段可以有不同类型。

重要边界是:anyOf 只决定组合阶段的结果,不会自动取消或停止其他仍在运行的分支。要实现“第一个成功结果”而不是“第一个完成结果”,必须显式为每个分支把失败转化为继续竞争的逻辑。


6. 组合关系与执行路径

下面的结构包含两个并行加载、一个合并、一个异步转换:

flowchart LR
    A[开始] --> B[异步加载用户]
    A --> C[异步加载权限]
    B --> D[thenCombine]
    C --> D
    D --> E[thenApplyAsync 构造视图]
    E --> F[最终结果]

执行过程是:

  1. BC 可以同时提交;
  2. D 等待 BC 都正常完成;
  3. D 的合并函数生成中间结果;
  4. E 把中间结果提交到指定异步执行器;
  5. 任意未被处理的异常都会沿依赖边传播到最终阶段。

组合阶段不是线程安全容器,也不是消息队列。它表示的是一张“完成依赖图”:阶段之间通过完成状态连接,而不是通过每个方法调用立即执行连接。


7. 异常传播的基本规则

假设:

CompletableFuture<String> source =
        CompletableFuture.failedFuture(
                new IllegalStateException("source failed"));

7.1 普通转换阶段会跳过函数

CompletableFuture<Integer> mapped =
        source.thenApply(String::length);

String::length 不会执行,mapped 也异常完成。

原因是 thenApply 的函数只接收正常结果。源阶段没有正常结果,就没有可传入的 String

7.2 handle 同时接收结果和异常

CompletableFuture<String> recovered =
        source.handle((value, error) -> {
            if (error != null) {
                return "fallback";
            }
            return value;
        });

handle 无论源阶段成功还是失败都会执行,并且返回一个新的结果。因此它既可以转换成功值,也可以恢复异常。

参数可能是:

  • 成功时:value != nullerror == null
  • 失败时:value == nullerror != null

如果业务结果本身允许 null,不要只依赖 value == null 判断成功或失败,应检查 error

7.3 exceptionally 只处理异常

CompletableFuture<String> recovered =
        source.exceptionally(error -> {
            System.err.println("failed: " + error);
            return "fallback";
        });

成功时,处理函数不执行;失败时,处理函数返回的值会成为新的正常结果。

如果恢复函数再次抛出异常,新的阶段仍然失败:

CompletableFuture<String> stillFailed =
        source.exceptionally(error -> {
            throw new RuntimeException("fallback also failed");
        });

7.4 whenComplete 观察但通常不改变结果

CompletableFuture<String> observed =
        source.whenComplete((value, error) -> {
            logResult(value, error);
        });

whenComplete 适合日志、指标、清理等旁路动作。源阶段成功时,它通常保留原值;源阶段失败时,它通常保留原异常。

但是,如果 whenComplete 自己抛出异常,可能导致结果阶段失败,或使原异常成为被抑制、包装的原因。因此清理和日志代码不能假定自身永远不会失败。

7.5 exceptionallyCompose:异常后的异步恢复

当恢复动作本身是异步的,应使用 exceptionallyCompose

CompletableFuture<String> result =
        source.exceptionallyCompose(error ->
                loadFromBackupAsync()
        );

如果错误处理函数返回 CompletableFuture<String>,使用 exceptionally 会得到嵌套阶段;exceptionallyCompose 才会把恢复阶段扁平化。


8. 异常传播的完整算例

CompletableFuture<String> pipeline =
        CompletableFuture
                .supplyAsync(() -> "42")
                .thenApply(Integer::parseInt)
                .thenApply(value -> {
                    if (value == 42) {
                        throw new IllegalArgumentException("42 is rejected");
                    }
                    return value;
                })
                .exceptionally(error -> {
                    Throwable cause = unwrap(error);
                    return "recovered from " + cause.getClass().getSimpleName();
                });

System.out.println(pipeline.join());

传播步骤如下:

  1. supplyAsync 正常完成,结果是字符串 "42"
  2. thenApply(Integer::parseInt) 正常完成,结果是整数 42
  3. 第二个 thenApply 抛出 IllegalArgumentException
  4. 该阶段异常完成;
  5. 后续 exceptionally 被触发;
  6. unwrap 去除可能存在的 CompletionException
  7. 恢复函数返回字符串,因此最终阶段正常完成。
static Throwable unwrap(Throwable error) {
    if (error instanceof CompletionException && error.getCause() != null) {
        return error.getCause();
    }
    return error;
}

异常包装的层数取决于观察方式和组合路径。生产代码通常应保留原始异常作为 cause,而不是仅记录最外层 CompletionException


9. 超时:让阶段按时结束,不等于停止任务

Java 9 起,CompletableFuture 提供了两个直接的超时方法。

9.1 orTimeout

CompletableFuture<String> future =
        CompletableFuture.supplyAsync(() -> slowOperation());

future.orTimeout(500, TimeUnit.MILLISECONDS);

如果源阶段在 500 毫秒内没有完成,orTimeout 会让这个阶段异常完成,原因是 TimeoutException

关键特征是:orTimeout 直接作用于并返回同一个 CompletableFuture

CompletableFuture<String> same = future.orTimeout(...);
System.out.println(same == future); // true

它改变的是阶段的可观察完成状态,不是底层操作本身。

9.2 completeOnTimeout

CompletableFuture<String> future =
        CompletableFuture.supplyAsync(() -> slowOperation())
                .completeOnTimeout("default", 500, TimeUnit.MILLISECONDS);

超时后,阶段正常完成为 "default",而不是异常完成。

因此两者的语义不同:

orTimeout       超时 -> 异常完成
completeOnTimeout 超时 -> 指定默认值正常完成

两者都存在竞争:

真实任务完成 ─┐
              ├── 第一个成功改变阶段状态的事件生效
超时事件   ───┘

如果真实任务在超时前完成,超时动作不会覆盖真实结果;如果超时先发生,后续真实结果也不会覆盖超时结果。

9.3 超时不会自动中断底层操作

CompletableFuture<String> timed =
        CompletableFuture
                .supplyAsync(() -> {
                    try {
                        Thread.sleep(2_000);
                        return "late";
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        throw new CancellationException("interrupted");
                    }
                })
                .orTimeout(100, TimeUnit.MILLISECONDS);

try {
    timed.join();
} catch (CompletionException e) {
    System.out.println(e.getCause()); // TimeoutException
}

100 毫秒后,timed 已经失败,但 sleep 所在的任务不必然停止。若底层是数据库查询、HTTP 调用或阻塞 I/O,还需要使用对应客户端的超时和取消机制。

阶段超时与资源超时必须分别设置:

CompletableFuture 超时:限制调用方等待多久
HTTP/数据库超时:限制底层资源占用多久

只设置第一层,可能出现“调用方已返回,数据库查询仍继续消耗连接”的情况。


10. 取消:取消的是阶段状态,不是任意底层工作

调用:

boolean cancelled = future.cancel(false);

如果阶段尚未完成,取消会使其以 CancellationException 异常完成。

CompletableFuture 的取消定义可以理解为:

future.completeExceptionally(new CancellationException());

取消不会自动产生一个新的正常结果,依赖该阶段的后续阶段通常也会异常完成。

10.1 取消依赖阶段的方向

CompletableFuture<String> source =
        new CompletableFuture<>();

CompletableFuture<Integer> dependent =
        source.thenApply(String::length);

dependent.cancel(false);

System.out.println(source.isDone()); // false

取消 dependent 不会反向取消 source。因为一个源阶段可能被多个消费者依赖,取消其中一个消费者不应破坏其他消费者。

反过来:

source.cancel(false);

System.out.println(dependent.isCompletedExceptionally()); // true

源阶段取消后,依赖阶段无法获得正常输入,因此会沿依赖图传播失败状态。

10.2 cancel(true) 不保证中断运行中的任务

Future.cancel(true) 的语义包含“如果任务正在运行,尝试中断”。但 CompletableFuture 本身不拥有一个可强制停止的计算线程。对由 supplyAsync 等方法创建的任务,取消 CompletableFuture 不等价于可靠地中断底层任务。

因此下面代码不能据此保证打印 "interrupted"

CompletableFuture<Void> task =
        CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(10_000);
            } catch (InterruptedException e) {
                System.out.println("interrupted");
            }
        });

task.cancel(true);

要实现协作式取消,应让任务主动检查取消信号:

class CancellationToken {
    private final AtomicBoolean cancelled = new AtomicBoolean();

    void cancel() {
        cancelled.set(true);
    }

    boolean isCancelled() {
        return cancelled.get();
    }
}

CancellationToken token = new CancellationToken();

CompletableFuture<Integer> task =
        CompletableFuture.supplyAsync(() -> {
            int total = 0;
            for (int i = 0; i < 1_000_000; i++) {
                if (token.isCancelled()) {
                    throw new CancellationException("operation cancelled");
                }
                total += i;
            }
            return total;
        });

token.cancel();

对可中断阻塞调用,还应同时保留并正确处理 InterruptedException。取消信号、线程中断、网络客户端取消是三个相关但不完全等价的机制。


11. 超时、取消和异常的统一故障路径

一个真实异步调用通常有多种结束原因:

stateDiagram-v2
    [*] --> Running
    Running --> Succeeded: 底层操作成功
    Running --> Failed: 底层操作抛异常
    Running --> TimedOut: orTimeout 先完成
    Running --> Cancelled: cancel 先完成
    TimedOut --> ResourceStillRunning: 底层操作未被停止
    Cancelled --> ResourceStillRunning: 底层操作未被停止
    Succeeded --> [*]
    Failed --> [*]

这里的 TimedOutCancelled 描述的是 CompletableFuture 的观察状态,不一定描述底层资源的真实状态。

例如:

  1. HTTP 请求任务仍在执行;
  2. orTimeout 让阶段以 TimeoutException 结束;
  3. 上层返回超时响应;
  4. HTTP 客户端仍可能占用连接;
  5. 如果没有底层请求超时或显式取消,资源最终才会释放。

因此错误处理通常要分两层:

CompletableFuture<Response> call =
        CompletableFuture
                .supplyAsync(() -> httpClientCall(), ioPool)
                .orTimeout(800, TimeUnit.MILLISECONDS)
                .whenComplete((response, error) -> {
                    if (error != null) {
                        recordFailure(error);
                    }
                });

这段代码只保证阶段在 800 毫秒内可观察地结束。httpClientCall() 自身仍应配置连接、读取和整体请求超时。


12. 一个可运行的端到端示例

下面示例展示:

  • 显式线程池;
  • 两个并行分支;
  • thenCombine 合并;
  • thenApplyAsync 切换执行器;
  • orTimeout
  • exceptionally 统一恢复;
  • join 末端取值;
  • 线程池生命周期关闭。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class CompletableFutureDemo {
    public static void main(String[] args) {
        ExecutorService ioPool = Executors.newFixedThreadPool(4);
        ExecutorService cpuPool = Executors.newFixedThreadPool(2);

        try {
            CompletableFuture<String> user =
                    CompletableFuture.supplyAsync(() -> {
                        sleep(150);
                        return "user-7";
                    }, ioPool);

            CompletableFuture<String> permission =
                    CompletableFuture.supplyAsync(() -> {
                        sleep(100);
                        return "READ";
                    }, ioPool);

            CompletableFuture<String> view =
                    user.thenCombine(permission,
                                    (u, p) -> u + ":" + p)
                        .thenApplyAsync(String::toLowerCase, cpuPool)
                        .orTimeout(1, TimeUnit.SECONDS)
                        .exceptionally(error -> {
                            Throwable cause = unwrap(error);

                            if (cause instanceof TimeoutException) {
                                return "timeout";
                            }
                            return "fallback:" +
                                    cause.getClass().getSimpleName();
                        });

            System.out.println(view.join());
        } finally {
            ioPool.shutdown();
            cpuPool.shutdown();
        }
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("interrupted", e);
        }
    }

    private static Throwable unwrap(Throwable error) {
        if (error instanceof java.util.concurrent.CompletionException
                && error.getCause() != null) {
            return error.getCause();
        }
        return error;
    }
}

在 Java 25 上编译运行:

javac CompletableFutureDemo.java
java CompletableFutureDemo

预期输出:

user-7:read

为什么这个输出成立:

  1. userpermissionioPool 中并行执行;
  2. 两者分别得到 "user-7""READ"
  3. thenCombine 生成 "user-7:READ"
  4. thenApplyAsynccpuPool 中转换为小写;
  5. 总耗时低于 1 秒,因此 orTimeout 不触发;
  6. exceptionally 没有执行;
  7. join 取得最终字符串。

如果把 sleep(150) 改成 sleep(2_000),可能出现 "timeout"。但对应的任务不一定会立即停止,这正是阶段超时与底层任务取消的区别。


13. 线程池选择与生命周期

13.1 公共线程池不是业务隔离边界

未显式指定执行器时,异步方法使用默认异步执行设施:

CompletableFuture.supplyAsync(this::load);
future.thenApplyAsync(this::parse);

这种写法适合短小、非阻塞、资源需求明确的任务。若把数据库、远程调用、大量阻塞等待都提交到公共线程池,某类慢任务可能影响同一进程中的其他异步任务。

13.2 不同资源使用不同执行器

常见划分是:

I/O 阻塞任务 -> I/O 线程池
CPU 密集计算 -> 大小接近 CPU 并行度的计算线程池
需要隔离的租户或业务 -> 独立资源池

这不是 CompletableFuture 的强制规则,而是资源隔离设计。线程池大小不能脱离任务类型、外部服务容量和排队策略单独决定。

13.3 自定义执行器必须关闭

ExecutorService executor = Executors.newFixedThreadPool(8);
try {
    // 提交并等待任务
} finally {
    executor.shutdown();
}

如果使用应用服务器、Spring 或其他容器管理的线程池,通常不应在业务代码中关闭它;如果由当前组件创建,则必须明确其所有权和关闭时机。

13.4 拒绝执行也会进入异常路径

当显式执行器关闭或队列拒绝任务时,异步阶段可能以 RejectedExecutionException 失败。不要只捕获业务函数内部的异常,也要考虑阶段提交失败。


14. 常见错误与诊断方法

错误一:把 thenApply 当作串行等待

future.thenApply(x -> blockingCall(x));

问题不在于结果一定错误,而在于阻塞动作的执行线程不透明。诊断时记录:

System.out.println(Thread.currentThread().getName());

并检查线程池队列、活动线程数、任务等待时间。

错误二:把 allOf 当作结果收集器

allOf 的结果是 Void。如果需要结果,必须保留各个输入阶段,或在每个阶段完成后写入明确的数据结构。不要在 allOf 正常完成前调用输入阶段的 join,否则可能提前阻塞。

错误三:只给 CompletableFuture 设置超时

看到 TimeoutException 只能证明阶段已经超时,不能证明底层连接、查询或计算已经停止。诊断时需要同时查看:

  • 异步阶段的完成状态;
  • 实际执行线程是否仍在运行;
  • HTTP/数据库客户端是否有自己的超时;
  • 连接池是否持续增长;
  • 任务是否响应中断或取消令牌。

错误四:用 whenComplete 做恢复

future.whenComplete((value, error) -> {
    if (error != null) {
        returnFallback();
    }
});

这里的 returnFallback() 的返回值不会成为阶段结果。需要恢复结果时使用 handleexceptionally

错误五:忘记异常会在末端才暴露

异步链中间阶段异常完成时,提交调用本身可能没有抛异常:

CompletableFuture<String> future =
        CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("failed");
        });

System.out.println("submission returned");

异常通常在 joinget 或异常处理阶段观察到。生产代码应确保每条业务链都有明确的末端观察点,否则失败可能只表现为“结果没有产生”。


15. 规范保证、实现行为与工程取舍

需要明确区分三类结论。

规范和 API 契约保证的内容:

  • 一个 CompletableFuture 只能最终完成一次;
  • thenCompose 用于异步阶段扁平化;
  • allOf 等待所有输入阶段完成,返回 Void 阶段;
  • orTimeout 超时后异常完成;
  • completeOnTimeout 超时后以指定值正常完成;
  • cancel 使阶段以取消异常完成;
  • 后续阶段默认继承前置阶段的异常完成状态。

常见实现行为:

  • CompletableFuture 的默认异步执行器通常是公共 ForkJoinPool
  • Async 动作可能由完成源阶段的线程执行;
  • 依赖动作可能在阶段已经完成时由注册线程直接执行。

这些行为不能被简化成“永远在某个固定线程执行”。

工程设计取舍:

  • 阻塞 I/O 是否使用独立线程池;
  • 是否在超时后继续允许底层任务运行;
  • 是否需要协作式取消;
  • 异常是向上传播、转换为默认值,还是转为降级调用;
  • 多个异步分支是否允许共享公共线程池。

CompletableFuture 提供的是完成状态和依赖关系模型,不会自动替应用管理线程、连接、请求生命周期或业务补偿。

理解这一点后,组合、线程池、超时、取消和异常传播可以统一起来:组合阶段定义依赖,线程池决定动作在哪里执行,超时和取消改变阶段的可观察状态,异常沿依赖图传播,底层资源是否停止则必须由具体执行器和客户端共同保证。


系列导航与关联阅读

官方资料

本文依据 Java、Spring 与相关项目官方文档重新梳理;正文、示例与生产清单由 WR BLOG 编写。