CompletableFuture 异步编程实战文档(含真实工作场景)

CompletableFuture 异步编程(Java)实战文档(含真实工作场景)

目标:让你在项目里把 "并发 + 依赖编排 + 超时兜底 + 线程池治理 + 可观测性" 这些坑一次踩完。

适用:Java 8+(强烈建议 Java 11/17);Spring Boot 项目可直接套用。


1. 你为什么需要 CompletableFuture(而不是手写 Thread / Future)

1.1 传统 Future 的痛点

  • Future.get() 阻塞,你很难做"A 完成后再做 B"这种依赖编排。
  • 异常处理很弱:要么吞掉,要么到 get() 时才爆。
  • 多个任务组合、竞速、聚合结果写起来很痛苦。

1.2 CompletableFuture 的核心价值

  • 声明式编排:thenApply/thenCompose/thenCombine/allOf/anyOf
  • 异步回调:不必阻塞等待
  • 统一异常通道:exceptionally / handle / whenComplete
  • 可组合:复杂业务可拆分为多个小 future,再组合回主流程

2. 心智模型:一个"流水线" + 两类函数 + 两种依赖关系

2.1 两类函数(同步 vs 异步)

  • thenApply(...) / thenCompose(...) / thenCombine(...)不带 Async
    默认在"上一步完成的线程"继续执行(可能是你线程池的线程,也可能是调用线程)
  • thenApplyAsync(...) / thenComposeAsync(...) / thenCombineAsync(...)带 Async
    会把该阶段提交到线程池(默认 ForkJoinPool.commonPool 或你传入的 executor)

2.2 两种依赖关系(非常重要)

  • map (1 -> 1 转换):thenApply
    上一步返回值 -> 计算 -> 新值
  • flatMap (1 -> future):thenCompose
    上一步返回值 -> 再发起异步 -> 新 future(避免 future 套 future)

3. 必备 API 速查表(项目里最常用的一小撮)

目的 API 说明
创建异步任务 supplyAsync / runAsync 有返回值用 supplyAsync
串行转换 thenApply 同步转换
串行再异步 thenCompose 返回 future 的场景
两个任务合并 thenCombine 等两边都好,合并结果
多任务聚合 allOf 等全部完成(无结果,要自己收集)
竞速取最快 anyOf 谁先完成用谁
异常兜底 exceptionally 出错返回默认值
异常/成功都能处理 handle 拿到 (result, ex)
只做收尾日志 whenComplete 不改变结果
超时 orTimeout / completeOnTimeout Java 9+
主动取消 cancel(true) 中断仅对可中断操作有效

4. 线程池:成败关键(默认 commonPool 很容易出事)

4.1 常见事故

  • 你把 RPC/HTTP/DB 阻塞 I/O 扔进 ForkJoinPool.commonPool
    → commonPool 是为计算型任务设计的,阻塞会拖垮全局异步,甚至影响别的组件。
  • 线程池没隔离:某个慢接口导致线程占满,整站都慢(典型雪崩)。

4.2 推荐:为业务异步单独建线程池(I/O 型)

  • 经验:
    • I/O 型:线程数可比 CPU 核数大很多(看外部依赖的平均耗时)
    • 计算型:线程数接近 CPU 核数
4.2.1 线程池配置模板
java 复制代码
import java.util.concurrent.*;

public class AsyncExecutors {
    public static ExecutorService ioPool() {
        int core = 32;      // 结合 QPS、RT、依赖耗时来算
        int max = 64;
        int queue = 2000;

        return new ThreadPoolExecutor(
            core, max,
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(queue),
            r -> {
                Thread t = new Thread(r);
                t.setName("cf-io-" + t.getId());
                t.setDaemon(true);
                return t;
            },
            new ThreadPoolExecutor.CallerRunsPolicy() // 兜底:让调用方执行,形成"背压"
        );
    }
}

CallerRunsPolicy 是一种"把压力推回去"的背压方式:队列满了就让上游慢下来,避免无限堆积。


5. 三种"工作中最常见"的异步模型(直接套用)

模型 A:并行调用多个下游,聚合结果(最常见)

例子:商品详情页 = 基础信息 + 库存 + 促销 + 评价摘要(4 个下游并行)

java 复制代码
ExecutorService pool = AsyncExecutors.ioPool();

CompletableFuture<BaseInfo> baseF = CompletableFuture.supplyAsync(() -> baseClient.queryBase(id), pool);
CompletableFuture<Stock> stockF = CompletableFuture.supplyAsync(() -> stockClient.queryStock(id), pool);
CompletableFuture<Promo> promoF = CompletableFuture.supplyAsync(() -> promoClient.queryPromo(id), pool);
CompletableFuture<ReviewSummary> reviewF = CompletableFuture.supplyAsync(() -> reviewClient.querySummary(id), pool);

CompletableFuture<ProductDetail> detailF =
    baseF.thenCombine(stockF, (base, stock) -> new PartialDetail(base, stock))
         .thenCombine(promoF, (partial, promo) -> partial.withPromo(promo))
         .thenCombine(reviewF, (partial, review) -> partial.withReview(review))
         .thenApply(PartialDetail::toDetail)
         .exceptionally(ex -> {
             // 兜底:降级返回
             return ProductDetail.fallback(id);
         });

ProductDetail detail = detailF.join(); // 在边界(Controller/Facade)再 join

要点

  • 并行提升吞吐:整体 RT 近似 max(各下游 RT)
  • 任何一个失败,要决定:整体失败局部降级(强烈建议局部降级)

模型 B:有依赖的异步链(下游 2 依赖下游 1 的结果)

例子:先查用户信息拿到 userLevel,再按等级查权益

java 复制代码
CompletableFuture<Benefits> benefitsF =
    CompletableFuture.supplyAsync(() -> userClient.getUser(userId), pool)
        .thenCompose(user -> CompletableFuture.supplyAsync(() -> benefitClient.query(user.level()), pool))
        .exceptionally(ex -> Benefits.empty());

要点

  • 这类必须用 thenCompose,不要 thenApply 返回 CompletableFuture<CompletableFuture<T>> 这种套娃。

模型 C:竞速(多线路谁快用谁)+ 超时

例子:读缓存(Redis)慢了就走数据库(或走备用集群)

java 复制代码
CompletableFuture<String> redisF =
    CompletableFuture.supplyAsync(() -> redis.get(key), pool)
        .completeOnTimeout(null, 30, TimeUnit.MILLISECONDS);

CompletableFuture<String> dbF =
    CompletableFuture.supplyAsync(() -> db.query(key), pool)
        .orTimeout(150, TimeUnit.MILLISECONDS);

CompletableFuture<String> resultF =
    redisF.thenCompose(v -> v != null
        ? CompletableFuture.completedFuture(v)
        : dbF
    ).exceptionally(ex -> "DEFAULT");

String v = resultF.join();

6. allOf 的正确打开方式(聚合 List 结果)

allOf 只给你一个 CompletableFuture<Void>,你要自己把结果从每个 future 里拿出来。

java 复制代码
List<Long> ids = List.of(1L, 2L, 3L);

List<CompletableFuture<User>> futures = ids.stream()
    .map(id -> CompletableFuture.supplyAsync(() -> userClient.getUser(id), pool)
        .exceptionally(ex -> User.fallback(id)))
    .toList();

CompletableFuture<Void> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));

List<User> users = all.thenApply(v ->
    futures.stream().map(CompletableFuture::join).toList()
).join();

要点

  • join()allOf 后面再 join,保证不会阻塞太久。
  • 每个子任务最好自己 exceptionally,否则一个失败会让 allOf 整体异常。

7. 异常处理:别让异常"穿透"到最后才炸

7.1 三种处理方式怎么选

  • exceptionally(ex -> fallback)只处理异常,返回替代值
  • handle((r, ex) -> ...)成功/失败都处理,想统一收口就用它
  • whenComplete((r, ex) -> log)只做副作用(日志/埋点),不改结果

7.2 建议:在"每个外部依赖"边界就兜底

java 复制代码
CompletableFuture<Stock> stockF =
    CompletableFuture.supplyAsync(() -> stockClient.queryStock(id), pool)
        .orTimeout(80, TimeUnit.MILLISECONDS)
        .exceptionally(ex -> Stock.unknown(id));

8. 超时与取消:别让任务无限挂着

8.1 超时(Java 9+)

  • orTimeout(x, unit):超时直接异常
  • completeOnTimeout(value, x, unit):超时返回默认值(更适合降级)

8.2 取消(注意:不是万能)

future.cancel(true) 只有在任务代码支持中断时才有效:

  • Thread.sleep、阻塞队列、部分 I/O 支持中断
  • HTTP 客户端/数据库驱动不一定立刻响应中断(看具体实现)

9. 在 Spring Boot 里怎么落地(推荐做法)

9.1 把线程池做成 Bean

java 复制代码
@Configuration
public class AsyncConfig {

    @Bean(destroyMethod = "shutdown")
    public ExecutorService cfIoPool() {
        return AsyncExecutors.ioPool();
    }
}

然后用:

java 复制代码
@Autowired ExecutorService cfIoPool;

CompletableFuture.supplyAsync(() -> callDownstream(), cfIoPool);

9.2 监控与告警(别裸奔)

至少要做到:

  • 线程池:activeCount、queue size、reject count(可用 Micrometer 采集)
  • 下游调用:RT、成功率、超时率
  • 降级次数:fallback 命中率

10. 可观测性:日志 MDC / TraceId 传递(经典坑)

如果你用 SLF4J MDC 或链路追踪(SkyWalking / Zipkin / OpenTelemetry),
异步线程不会自动带上上下文

10.1 简单做法:封装 Runnable / Supplier

java 复制代码
public static <T> Supplier<T> wrapMdc(Supplier<T> task, Map<String, String> ctx) {
    return () -> {
        Map<String, String> old = MDC.getCopyOfContextMap();
        if (ctx != null) MDC.setContextMap(ctx);
        try { return task.get(); }
        finally {
            if (old != null) MDC.setContextMap(old);
            else MDC.clear();
        }
    };
}

// 用法
Map<String, String> ctx = MDC.getCopyOfContextMap();
CompletableFuture.supplyAsync(wrapMdc(() -> callDownstream(), ctx), pool);

更工程化的方案:用 TaskDecorator(Spring)或 OpenTelemetry 的 context propagation。


11. 性能与稳定性建议(血泪版)

  1. 不要把阻塞 I/O 放 commonPool。
  2. 线程池要隔离:按业务域/下游依赖分组(库存池、营销池、用户池)。
  3. 每个外部依赖都要:超时 + 降级,不要让单点拖死链路。
  4. 批量任务用 allOf 时,要控制并发(别一次性 1w 个 future)。
    • 简单控制:分批 + join
    • 更高级:Semaphore/限流器/自研批处理器
  5. join() 放在边界层:Controller/Facade/任务调度入口。
  6. 降级不是"返回空"就完事:要配合埋点,否则你以为系统稳定,实际上是数据烂了。

12. 真实工作场景清单(你可以对号入座)

12.1 电商/支付

  • 订单确认页:地址 + 优惠券 + 运费 + 库存
  • 支付前风控:黑名单/设备指纹/风险评分并行
  • 支付成功后:异步触发(发货、积分、通知、推荐),但要幂等

12.2 营销/推荐

  • 多路召回并行(内容、协同过滤、热榜),聚合排序
  • A/B 实验:多策略竞速,选最先完成或最佳评分

12.3 账户/风控

  • KYC:多第三方(身份证/活体/反欺诈)并行校验
  • 风险核验链:有依赖的异步链(先查画像再算策略)

12.4 管理后台/报表

  • 多维度指标查询并行(各业务表/ES/缓存),组装报表
  • 批量导出:并行拉取 + 分批写入文件(注意限流、内存)

13. 进阶:并发控制(防止一口气创建太多 future)

13.1 用 Semaphore 做"批量下游调用限流"

java 复制代码
Semaphore sem = new Semaphore(50); // 最多并发50个

CompletableFuture<User> f = CompletableFuture.supplyAsync(() -> {
    try {
        sem.acquire();
        return userClient.getUser(id);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return User.fallback(id);
    } finally {
        sem.release();
    }
}, pool);

14. 常见反模式(看到就改)

  • ❌ 在 thenApply 里做阻塞网络调用(会占住回调线程)
    ✅ 用 thenCompose + supplyAsync
  • allOf 不做子任务兜底 → 一个失败全失败
    ✅ 每个任务自己 exceptionally
  • ❌ 无超时 → 线程被挂死
    orTimeout/completeOnTimeout
  • ❌ 线程池无隔离/无队列上限 → OOM/雪崩
    ✅ 有界队列 + 拒绝策略 + 背压
  • ❌ 异步后 MDC/TraceId 丢失 → 查日志像盲人摸象
    ✅ 上下文传递

15. 一页总结(你可以贴到团队 wiki)

  • CompletableFuture 本质是 异步任务 + 可组合的编排 DSL
  • 业务最常用三件套:
    1. 并行聚合(thenCombine / allOf)
    2. 有依赖链(thenCompose)
    3. 超时降级(completeOnTimeout + exceptionally)
  • 线程池治理决定你是"提速"还是"自杀"。
  • 异步不是为了炫技,是为了:把整体 RT 从"求和"变成"取最大"

附录:可复制的"聚合接口"骨架(Controller/Facade 层)

java 复制代码
public ProductDetail queryDetail(long id, ExecutorService pool) {

    CompletableFuture<BaseInfo> baseF = CompletableFuture
            .supplyAsync(() -> baseClient.queryBase(id), pool)
            .completeOnTimeout(BaseInfo.fallback(id), 80, TimeUnit.MILLISECONDS)
            .exceptionally(ex -> BaseInfo.fallback(id));

    CompletableFuture<Stock> stockF = CompletableFuture
            .supplyAsync(() -> stockClient.queryStock(id), pool)
            .completeOnTimeout(Stock.unknown(id), 60, TimeUnit.MILLISECONDS)
            .exceptionally(ex -> Stock.unknown(id));

    CompletableFuture<Promo> promoF = CompletableFuture
            .supplyAsync(() -> promoClient.queryPromo(id), pool)
            .completeOnTimeout(Promo.none(), 60, TimeUnit.MILLISECONDS)
            .exceptionally(ex -> Promo.none());

    return baseF.thenCombine(stockF, (base, stock) -> new PartialDetail(base, stock))
            .thenCombine(promoF, (p, promo) -> p.withPromo(promo))
            .thenApply(PartialDetail::toDetail)
            .join();
}

相关推荐
charlee4414 天前
为什么协程能让程序不再卡顿?——从同步、异步到 C++ 实战
qt·协程·异步编程·gui卡顿·boost.coroutine2
十五年专注C++开发19 天前
同一线程有两个boost::asio::io_context可以吗?
c++·boost·asio·异步编程·io_context
啊Q老师2 个月前
Rust:异步编程与并发安全的深度实践
rust·并发安全·异步编程·深度实践
鼓掌MVP2 个月前
Rust Web实战:构建高性能并发工具的艺术
开发语言·前端·rust·异步编程·内存安全·actix-web·高性能web服务
熊猫钓鱼>_>2 个月前
Rust语言特性深度解析:所有权、生命周期与模式匹配之我见
算法·rust·软件开发·函数·模式匹配·异步编程·质量工具
奔跑吧邓邓子3 个月前
【C++实战(54)】C++11新特性实战:解锁原子操作与异步编程的奥秘
c++·实战·c++11新特性·原子操作·异步编程
佛祖让我来巡山3 个月前
【CompletableFuture 核心操作全解】详细注释版
异步编程·completablefuture
工程师0075 个月前
C#多线程,同步与异步详解
开发语言·c#·多线程·同步·异步编程
佛祖让我来巡山5 个月前
【CompletableFuture 终极指南】从原理到生产实践
异步编程·completablefuture