Lesson 14 · 并发编程

CompletableFuture 异步编排:从回调地狱到流式组合

中级·#并发·#异步

第 1 站

开场:商品详情页为什么要 800ms?

"一个商品详情页,用户打开后需要等多久才能看到完整内容?"

打开某电商 App 的商品详情页,页面渲染需要 4 个后端数据:

数据模块调用方式平均耗时
商品基础信息DB 查询200ms
价格与优惠价格中心 RPC200ms
用户评价评价服务 RPC200ms
推荐商品推荐引擎 RPC200ms

如果按顺序串行调用,总耗时 = 4 × 200ms = 800ms。但实际上这 4 个调用之间完全没有依赖关系——它们可以同时发起,等全部完成后再组装页面。

串行 vs 并行——耗时对比
// 串行:800ms
Product product  = productService.getById(id);       // 200ms
Price   price    = priceService.getPrice(id);        // 200ms
List<Review> reviews = reviewService.list(id);   // 200ms
List<Recommend> recs = recommendService.list(id); // 200ms
// 总计 ≈ 800ms

// 并行 + 编排:≈ 300ms(max + 组装开销)
CompletableFuture<Product>   f1 = supplyAsync(() -> productService.getById(id));
CompletableFuture<Price>     f2 = supplyAsync(() -> priceService.getPrice(id));
CompletableFuture<List<Review>>   f3 = supplyAsync(() -> reviewService.list(id));
CompletableFuture<List<Recommend>> f4 = supplyAsync(() -> recommendService.list(id));

CompletableFuture.allOf(f1, f2, f3, f4).join();  // 等全部完成 ≈ 200ms
DetailVO vo = assemble(f1.get(), f2.get(), f3.get(), f4.get()); // 组装 ≈ 100ms
// 总计 ≈ 300ms
面试考点

CompletableFuture 是 JDK 8 引入的异步编程利器,面试官考它的原因:① 生产环境中"多 RPC 聚合"是最常见的 IO 场景;② 它能体现你对异步编排、异常处理、线程池协作的综合理解;③ 用得好能直接把接口 RT 砍到原来的 1/N。

第 2 站

创建 CompletableFuture:三种姿势

创建 CompletableFuture 有三种常用方式,核心区别在于是否有返回值以及是否已经完成了

三种创建方式对比
// ① supplyAsync —— 有返回值(Supplier → CompletableFuture<T>)
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
    // 耗时操作,最终返回结果
    return productService.getById(123).getName();
});

// ② runAsync —— 无返回值(Runnable → CompletableFuture<Void>)
CompletableFuture<Void> f2 = CompletableFuture.runAsync(() -> {
    // 只执行操作,不返回结果(如发通知、写日志)
    notifyService.send("order_created");
});

// ③ completedFuture —— 已经完成的 Future(常用于测试或默认值)
CompletableFuture<String> f3 = CompletableFuture.completedFuture("cached-value");
f3.isDone();  // true,立即完成

线程池选择:supplyAsync / runAsync 不传 Executor 时,默认使用 ForkJoinPool.commonPool()(线程数 = CPU 核数 - 1,且全 JVM 共享)。生产环境必须传入自定义线程池,避免互相干扰:

生产环境:传入自定义线程池
ExecutorService bizPool = new ThreadPoolExecutor(16, 32, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1000),
    new ThreadFactoryBuilder().setNameFormat("biz-async-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy());

CompletableFuture<Product> f = CompletableFuture.supplyAsync(
    () -> productService.getById(123), bizPool);  // ← 第二个参数指定线程池
核心记忆

supplyAsync 有返回值,runAsync 无返回值,completedFuture 立即可用。生产环境必须传入自定义 Executor,不要用默认的 ForkJoinPool.commonPool()——线程数不可控且互相干扰。

第 3 站

链式转换:thenApply / thenCompose / thenAccept

CompletableFuture 最强大的特性是链式组合——像 Stream 一样把多个异步步骤串成流水线。面试最爱考的三个方法:thenApplythenComposethenAccept

thenApply vs thenCompose——最关键的区别
// thenApply:同步转换(Function<T, U> → CompletableFuture<U>)
// 类比 Stream.map():输入 T,输出 U
CompletableFuture<String> nameFuture =
    supplyAsync(() -> productService.getById(123))   // CompletableFuture<Product>
    .thenApply(product -> product.getName());           // CompletableFuture<String>

// thenCompose:扁平化转换(Function<T, CompletableFuture<U>> → CompletableFuture<U>)
// 类比 Stream.flatMap():下一步本身也是异步的,需要"拍平"嵌套
CompletableFuture<Price> priceFuture =
    supplyAsync(() -> productService.getById(123))       // CompletableFuture<Product>
    .thenCompose(product ->
        priceService.getPriceAsync(product.getSkuId()));   // 返回 CompletableFuture<Price>
    // 如果用 thenApply,结果会是 CompletableFuture<CompletableFuture<Price>> ← 嵌套!
thenApplyAsync / thenAccept / thenRun
// thenApply(同步转换)vs thenApplyAsync(异步转换)
f.thenApply(result -> result.toUpperCase());         // 在上一步线程中同步执行
f.thenApplyAsync(result -> callRPC(result));           // 提交到线程池异步执行(转换本身耗时)

// thenAccept:消费结果,无返回值(类比 forEach)
supplyAsync(() -> orderService.create(dto))
    .thenAccept(orderId -> log.info("订单创建成功: {}", orderId));

// thenRun:不关心上一步结果,只执行后续动作
supplyAsync(() -> orderService.create(dto))
    .thenRun(() -> metricsService.increment("order.created"));
thenApplymap  |  thenComposeflatMap  |  thenAcceptforEach
面试高频追问

"thenApply 和 thenCompose 的区别是什么?" 回答要点:thenApply 的回调返回普通值 U,thenCompose 的回调返回 CompletableFuture<U>。如果下一步也是异步操作(返回 Future),必须用 thenCompose 来"拍平",否则会得到嵌套的 CompletableFuture<CompletableFuture<T>>

第 4 站

组合与聚合:thenCombine / allOf / anyOf

链式转换解决的是"串行流水线",但真实场景中经常需要多个独立 Future 的聚合。这就是 thenCombineallOfanyOf 的舞台。

thenCombine Future A Future B A + B → C allOf F1 F2 F3 F4 全部完成 anyOf F1 F2 F3 F4 最先完成 两个 Future 合并为一个结果 等待所有 Future 完成 任一 Future 完成即返回 实战:商品 + 价格 → 商品详情 VO Product Future Price Future thenCombine → VO
图 1 CompletableFuture 三种组合模式——thenCombine / allOf / anyOf
thenCombine:两个 Future 合并为一个结果
CompletableFuture<Product> productF = supplyAsync(() -> productService.getById(id));
CompletableFuture<Price>   priceF   = supplyAsync(() -> priceService.getPrice(id));

CompletableFuture<DetailVO> detailF = productF.thenCombine(priceF, (product, price) -> {
    DetailVO vo = new DetailVO();
    vo.setName(product.getName());  vo.setPrice(price.getAmount());
    return vo;
});
allOf:等待全部完成
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3, f4);
all.thenRun(() -> {
    // 所有 Future 已完成,安全 get()
    DetailVO vo = assemble(f1.join(), f2.join(), f3.join(), f4.join());
    response.complete(vo);
});
// 注意:allOf 返回 CompletableFuture<Void>,不携带结果,必须手动 join
anyOf:最先完成的胜出
// 多数据源竞速:谁先返回用谁(常用于缓存 + DB 双读)
CompletableFuture<String> fromCache = supplyAsync(() -> cacheService.get(key));
CompletableFuture<String> fromDB = supplyAsync(() -> dbService.query(key));
CompletableFuture<Object> fastest = CompletableFuture.anyOf(fromCache, fromDB);
// 注意:anyOf 返回 CompletableFuture<Object>,需要强转
核心记忆

thenCombine 是"两路合并",allOf 是"等所有",anyOf 是"抢最快"。allOf 返回 Void 需要手动取结果,anyOf 返回 Object 需要强转。

第 5 站

异常处理:exceptionally / handle / whenComplete

异步编程最容易被忽视的就是异常处理。CompletableFuture 中的异常不会抛到调用方线程——如果你不主动处理,它会静默吞掉,直到你调 get() / join() 时才以 CompletionException 的形式爆发。

三种异常处理方式对比
// ① exceptionally —— 异常兜底,返回同类型默认值
CompletableFuture<List<Review>> reviewsF =
    supplyAsync(() -> reviewService.list(productId))
    .exceptionally(ex -> {
        log.warn("评价服务异常", ex);
        return Collections.emptyList();  // ← 必须返回同类型
    });
// ② handle —— 成功和异常都处理,可返回不同类型
CompletableFuture<String> resultF = supplyAsync(() -> riskyCall())
    .handle((result, ex) -> ex != null ? "fallback" : result);
// ③ whenComplete —— 副作用(日志/埋点),不改变结果
supplyAsync(() -> orderService.create(dto))
    .whenComplete((orderId, ex) -> {
        if (ex != null) log.error("创建订单失败", ex);
        else log.info("创建订单成功: {}", orderId);
    });  // 结果不变
方法输入输出是否改变结果典型场景
exceptionallyThrowable同类型 T是(兜底值)降级返回默认值
handle(T, Throwable)新类型 U成功/失败都需要转换
whenComplete(T, Throwable)同类型 T日志、埋点、监控
常见踩坑:exceptionally 的返回类型

exceptionally 的回调必须返回与原 Future 相同泛型类型的值。新手常犯的错误是在 exceptionally 中写 return null,导致下游收到 null 引发 NPE。正确做法是返回有意义的默认值(如空列表、空对象)。

异常传播链——链式调用中的异常会"穿透"
// 异常会跳过中间步骤,直接传到最近的 exceptionally
supplyAsync(() -> { throw new RuntimeException("boom"); })
    .thenApply(s -> s.toUpperCase())   // ← 跳过
    .thenCompose(s -> callRPC(s))      // ← 跳过
    .exceptionally(ex -> { log.error("链路异常", ex); return "default"; });
核心记忆

exceptionally 是兜底降级(必须返回同类型值),handle 是成功/失败双通道处理,whenComplete 是只读副作用。链式调用中异常会"穿透"中间步骤,直到被 exceptionally 或 handle 捕获。

第 6 站

超时控制:JDK 9+ 与 JDK 8 的不同做法

生产环境中,异步调用必须有超时——否则一个下游服务 hang 住,你的线程会被无限阻塞,最终线程池耗尽、服务雪崩。

JDK 9+:orTimeout 与 completeOnTimeout
// orTimeout —— 超时则标记为 TimeoutException
CompletableFuture<Product> f = supplyAsync(() -> productService.getById(id))
    .orTimeout(500, TimeUnit.MILLISECONDS);
// 500ms 内未完成 → future 以 TimeoutException 完成
// 后续 exceptionally/handle 可以捕获该异常

// completeOnTimeout —— 超时则用默认值完成(不会抛异常)
CompletableFuture<List<Review>> reviewsF =
    supplyAsync(() -> reviewService.list(id))
    .completeOnTimeout(Collections.emptyList(), 300, TimeUnit.MILLISECONDS);
// 300ms 内未完成 → 自动用空列表完成,下游正常执行
方法超时后行为适用场景
orTimeout以 TimeoutException 完成超时需要报错 / 触发 exceptionally
completeOnTimeout以指定默认值完成超时降级返回默认值(推荐评价、推荐等非核心模块)
JDK 8 兼容方案:ScheduledExecutorService
// JDK 8 没有 orTimeout / completeOnTimeout,需手动实现
private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

public static <T> CompletableFuture<T> withTimeout(
        CompletableFuture<T> future, long timeout, TimeUnit unit) {
    scheduler.schedule(() ->
        future.completeExceptionally(new TimeoutException("timed out")), timeout, unit);
    return future;  // complete 幂等,Future 已完成则此调用无效
}

CompletableFuture<Product> f = withTimeout(
    supplyAsync(() -> productService.getById(id)), 500, TimeUnit.MILLISECONDS);

关键点:complete()completeExceptionally() 都是幂等的——只有第一次调用生效。如果 Future 已经正常完成,后续的超时回调调 completeExceptionally 会被忽略,这让手动超时方案非常安全。

核心记忆

JDK 9+ 用 orTimeout(超时异常)和 completeOnTimeout(超时降级)。JDK 8 用 ScheduledExecutorService + completeExceptionally 手动实现。生产环境每个异步调用都必须设超时

第 7 站

生产实战模式:Pipeline / Fan-out Fan-in / Timeout+Retry

面试中如果能写出以下完整模式,会非常有加分。我们用商品详情页串联三种核心编排模式。

完整实战:商品详情页编排(Pipeline + Fan-out Fan-in + 超时降级)
public CompletableFuture<DetailVO> getProductDetail(long productId) {
    // Pipeline:查用户 → 查会员价(有依赖,用 thenCompose)
    CompletableFuture<PriceVO> priceF = supplyAsync(() -> userService.getCurrentUser(), pool)
        .thenCompose(user -> priceService.getMemberPriceAsync(productId, user.getLevel()))
        .orTimeout(300, TimeUnit.MILLISECONDS)
        .exceptionally(ex -> priceService.getDefaultPrice(productId));

    // Fan-out:商品、评价、推荐无依赖,并行发起
    CompletableFuture<Product> productF = supplyAsync(() -> productService.getById(productId), pool)
        .orTimeout(200, TimeUnit.MILLISECONDS);
    CompletableFuture<List<Review>> reviewsF = supplyAsync(() -> reviewService.list(productId), pool)
        .completeOnTimeout(Collections.emptyList(), 300, TimeUnit.MILLISECONDS);
    CompletableFuture<List<Recommend>> recsF = supplyAsync(() -> recommendService.list(productId), pool)
        .completeOnTimeout(Collections.emptyList(), 300, TimeUnit.MILLISECONDS);

    // Fan-in:聚合所有结果
    return CompletableFuture.allOf(productF, priceF, reviewsF, recsF)
        .thenApply(v -> {
            DetailVO vo = new DetailVO();
            vo.setProduct(productF.join());  vo.setPrice(priceF.join());
            vo.setReviews(reviewsF.join());  vo.setRecommendations(recsF.join());
            return vo;
        });
}
模式 3:Timeout + Retry(超时重试)
public static <T> CompletableFuture<T> withRetry(
        Supplier<CompletableFuture<T>> action, int maxRetries, long timeoutMs) {
    return action.get()
        .orTimeout(timeoutMs, TimeUnit.MILLISECONDS)
        .exceptionally(ex -> {
            if (maxRetries > 0) return withRetry(action, maxRetries - 1, timeoutMs).join();
            throw new CompletionException("重试耗尽", ex);
        });
}
// 使用:最多重试 2 次,每次超时 500ms
CompletableFuture<Product> f = withRetry(
    () -> supplyAsync(() -> productService.getById(id), pool), 2, 500);
模式特点适用场景
PipelinethenCompose 串联,前一步输出是后一步输入有依赖的多步异步(如:查用户 → 查会员价)
Fan-out / Fan-inallOf 并行发起,等全部完成无依赖的多路聚合(如:商品 + 评价 + 推荐)
Timeout + RetryorTimeout + exceptionally 递归重试不稳定的下游调用,需要容错
面试官追问:如果推荐服务挂了怎么办?

推荐是非核心模块。用 completeOnTimeout 超时降级为空列表 + exceptionally 捕获异常也返回空列表。核心模块(商品、价格)才需要重试。原则:核心模块重试 + 超时,非核心模块降级 + 超时

第 8 站

总结:方法速查表 + 常见坑

CompletableFuture 方法速查

类别方法一句话说明Stream 类比
创建supplyAsync有返回值的异步任务
runAsync无返回值的异步任务
completedFuture已完成的 Future(测试/默认值)Stream.of()
链式转换thenApply同步转换 T → Umap
thenCompose异步扁平转换 T → CF<U>flatMap
thenAccept / thenRun消费结果 / 执行动作forEach
组合聚合thenCombine两个 Future 合并为一个结果
allOf等所有 Future 完成
anyOf最先完成的胜出
异常处理exceptionally异常兜底,返回同类型默认值
handle成功/失败双通道处理
whenComplete只读副作用(日志/埋点)peek
超时 (9+)orTimeout超时 → TimeoutException
completeOnTimeout超时 → 默认值
常见坑清单
  1. 忘记传自定义线程池——默认 ForkJoinPool.commonPool() 线程数 = CPU-1,IO 密集场景必炸
  2. allOf 返回 Void——不能直接从 allOf 拿结果,必须手动 join() 每个子 Future
  3. exceptionally 返回 null——下游会收到 NPE,应返回有意义的默认值
  4. 没有设超时——下游 hang 住会导致线程永久阻塞,最终线程池耗尽
  5. thenApply vs thenCompose 混用——下一步是异步操作用 thenCompose,否则得到嵌套 Future
  6. join() vs get()——join() 抛 unchecked CompletionException,get() 抛 checked ExecutionException;链式调用中推荐 join()
面试 30 秒总结

"CompletableFuture 是 JDK 8 的异步编排框架。supplyAsync 创建有返回值的异步任务,thenApply 做同步转换(类比 map),thenCompose 做异步扁平转换(类比 flatMap),thenCombine 合并两个 Future,allOf 等全部完成,anyOf 抢最快。异常处理用 exceptionally 兜底降级、handle 双通道处理、whenComplete 做日志副作用。JDK 9 增加了 orTimeout 和 completeOnTimeout 做超时控制。生产环境三个核心原则:必须传自定义线程池、每个异步调用必须设超时、非核心模块做降级而非重试。"