JAVA CompletableFuture异步编程实战

10 · CompletableFuture 异步编程实战

对应学习计划:第一月第2周「Java 并发编程(重中之重!薪资分水岭)」

面试定位:CompletableFuture 是「接口异步化提升吞吐」的标配答案(学习计划里的项目亮点「订单接口异步化,吞吐量提升4倍+」就是它),也是 3 年+ Java 的高级面试点。


一、本文能帮你回答的面试题

  1. CompletableFuture 是什么?解决了 Future 的什么问题?
  2. supplyAsync/runAsync 用的默认线程池是什么?有什么坑?
  3. thenApply vs thenCompose vs thenCombine 区别?
  4. thenApply 和 thenApplyAsync 区别?
  5. 怎么处理 CompletableFuture 的异常?(exceptionally / handle / whenComplete)
  6. allOf / anyOf 怎么用?怎么收集结果?
  7. 接口异步化怎么做才能真提升吞吐?有哪些坑?

二、概念介绍

Future 的三大缺点(CompletableFuture 要解决的):

  1. get() 会阻塞,等不到结果前干不了别的。
  2. 无法「串联」多个异步任务(等 A 完成再用 A 的结果跑 B)。
  3. 没有异常处理机制(异常只能从 get 抛出来,处理时机不可控)。

CompletableFuture = Future + 回调(函数式串联):

  • 实现了 CompletionStage 接口,支持链式编排。
  • JDK8 提供,底层线程调用 + 内部 CompletableFuture 状态机(栈式依赖触发回调)。

一句话:把「异步 + 回调 + 组合」从业务代码里解放出来,像写同步代码一样编排异步任务。


三、原理深入

3.1 创建方式与默认线程池

java 复制代码
// 无返回值(Runnable)
CompletableFuture<Void> a = CompletableFuture.runAsync(() -> System.out.println("跑一下"));

// 有返回值(Supplier)
CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> 42);

// 推荐:多参数重载,指定自己的线程池!
Executor pool = Executors.newFixedThreadPool(8);
CompletableFuture.supplyAsync(() -> dao.query(), pool);

⚠️ 默认线程池是全局公共 ForkJoinPool.commonPool():

  • 大小 = CPU 核数 - 1。
  • 被所有 CompletableFuture、parallelStream 等共享。
  • 遇到 IO 阻塞任务会互相拖累,甚至饿死。

生产铁律:必须传自定义线程池(IO 密集场景线程数按业务配,独立隔离)。

3.2 核心方法族(面试重点,一图流)

复制代码
supplyAsync(生产) → thenApply(转换) → thenCompose(串联异步) → thenAccept(消费) → 结束
                    ├─ thenCombine(并行汇合两个 CompletableFuture)
                    ├─ thenApplyAsync(换线程池异步执行)
                    └─ exceptionally/handle/whenComplete(异常处理)
方法 语义 对比
thenApply(fn) 上一个结果 → 新结果(同步执行) 串行转换
thenApplyAsync 结果 → 异步线程池执行 换线程
thenCompose(fn) 上一个结果 → 返回新的 CompletableFuture 拍平嵌套
thenCombine(other, fn) 两个 CF 都完成后合并 并行汇聚
thenAccept/thenRun 消费结果 / 不关心结果 终点
exceptionally(fn) 异常时返回兜底值 异常处理
handle(fn) 无论成败都处理(参数带异常) 异常处理
whenComplete(fn) 完成后回调(不改返回值) 副作用
allOf(...) 等所有完成(Void) 并发并等待
anyOf(...) 等最早完成的一个 竞速

3.3 thenApply vs thenCompose(易混,必须分清)

java 复制代码
// thenApply:fn 返回普通值 → 结果是 CompletableFuture<U>
CompletableFuture<String> a = supplyAsync(() -> "a")
        .thenApply(s -> s.toUpperCase());      // 同步转换

// thenCompose:fn 返回 CompletableFuture → 结果会被「拍平」为一个 CF
CompletableFuture<String> b = supplyAsync(() -> "a")
        .thenCompose(s -> supplyAsync(() -> s + "!b"));   // 避免 CF<CF<String>>

// 类比:thenApply ≈ flatMap 前的 map;thenCompose ≈ flatMap(拍平)

错误示范(嵌套地狱) :.thenApply(s -> supplyAsync(...)) 生成 CompletableFuture<CompletableFuture<String>>,后续都得 get 两层。正确用 thenCompose。

3.4 异常处理三兄弟

java 复制代码
// exceptionally:只在异常时给兜底值
CompletableFuture.supplyAsync(() -> 1 / 0)
        .exceptionally(ex -> {
            System.out.println("异常: " + ex.getMessage());
            return -1;                          // 兜底
        });

// handle:成功/失败都执行,异常通过参数传入(返回自定义)
CompletableFuture.supplyAsync(() -> 1 / 0)
        .handle((v, ex) -> ex == null ? v : -1);

// whenComplete:只做「记录/通知」,不改结果,异常会继续向下传播
CompletableFuture.supplyAsync(() -> 1 / 0)
        .whenComplete((v, ex) -> System.out.println("完成,异常=" + ex))
        .exceptionally(ex -> -1);               // 必须再接 exceptionally 收敛

3.5 背后原理(一句话版)

CompletableFuture 内部是先进无锁栈:依赖节点(依赖回调)通过 CAS 压栈,完成时出栈触发;封装了 ForkJoinPool/线程池执行 + 状态机(PENDING→COMPLETED/EXPIRED)。面试答到「状态机 + 栈式回调 + 线程池执行」就够。


四、实战例子

实战 1:订单详情页「并行聚合查询」(面试项目级)

场景:查订单详情需要 用户信息 + 商品信息 + 物流信息 + 优惠信息,串行 400ms,并行只要 100ms。

java 复制代码
import java.util.concurrent.*;

public class ParallelAggregateDemo {
    static final Executor BIZ_POOL =
            new ThreadPoolExecutor(8, 16, 60, TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(1000),
                    new ThreadPoolExecutor.CallerRunsPolicy());

    public static void main(String[] args) throws Exception {
        long start = System.nanoTime();

        CompletableFuture<UserDTO> user = CompletableFuture.supplyAsync(() -> queryUser(), BIZ_POOL);
        CompletableFuture<ProductDTO> product = CompletableFuture.supplyAsync(() -> queryProduct(), BIZ_POOL);
        CompletableFuture<LogisticsDTO> logistics = CompletableFuture.supplyAsync(() -> queryLogistics(), BIZ_POOL);

        CompletableFuture<OrderDetailVO> detail = user
                .thenCombine(product, (u, p) -> new OrderDetailVO(u, p))     // 两两合并
                .thenCombine(logistics, (vo, l) -> { vo.logistics = l; return vo; });

        OrderDetailVO vo = detail.get(3, TimeUnit.SECONDS);
        System.out.println("并行耗时: " + (System.nanoTime() - start) / 1_000_000 + " ms → " + vo);
    }

    static UserDTO queryUser() { sleep(100); return new UserDTO("张三"); }
    static ProductDTO queryProduct() { sleep(100); return new ProductDTO("手机"); }
    static LogisticsDTO queryLogistics() { sleep(100); return new LogisticsDTO("已发货"); }

    record UserDTO(String name) {}
    record ProductDTO(String title) {}
    record LogisticsDTO(String status) {}
    static class OrderDetailVO {
        UserDTO user; ProductDTO product; LogisticsDTO logistics;
        OrderDetailVO(UserDTO u, ProductDTO p) { user = u; product = p; }
        @Override public String toString() {
            return "订单详情[用户=" + user + ", 商品=" + product + ", 物流=" + logistics + "]";
        }
    }

    static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException ignored) {} }
}
// 串行 300ms → 并行 ≈ 100ms,吞吐 ≈ 3x

实战 2:批量任务 + allOf 收集结果

java 复制代码
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Collectors;

public class AllOfDemo {
    public static void main(String[] args) throws Exception {
        Executor pool = Executors.newFixedThreadPool(4);
        List<Integer> ids = List.of(1, 2, 3, 4, 5);

        // 并发查询所有 id 对应数据
        List<CompletableFuture<String>> futures = ids.stream()
                .map(id -> CompletableFuture.supplyAsync(() -> fetch(id), pool))
                .collect(Collectors.toList());

        // allOf:全部完成,再收集(join 不抛 InterruptedException,get 会)
        CompletableFuture<Void> all = CompletableFuture.allOf(
                futures.toArray(new CompletableFuture[0]));
        all.join();

        List<String> results = futures.stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList());
        System.out.println(results);
    }

    static String fetch(int id) {
        try { Thread.sleep(50); } catch (InterruptedException ignored) {}
        return "数据" + id;
    }
}

实战 3:下单接口「先快速响应,再异步处理」(吞吐提升 4 倍核心)

java 复制代码
// 场景:下单成功后,发送短信 + 更新统计 + 发优惠券 ------ 都同步做会拖慢接口
public OrderVO createOrder(OrderReq req) {
    // ① 主流程:校验 + 扣库存 + 落库(必须同事务、同步)
    Order order = orderService.createWithLock(req);

    // ② 异步化:拿 orderId 交给线程池,主线程立即返回
    CompletableFuture.runAsync(() -> {
        smsService.sendSms(order.getUserId(), order.getId());       // 耗时 50ms
        statService.incr(order);                                    // 耗时 80ms
        couponService.grant(order.getUserId());                     // 耗时 60ms
        mqService.pushOrderCreated(order);                          // 耗时 20ms
    }, ASYNC_POOL);                                                 // 关键:独立线程池

    return OrderVO.from(order);   // 接口 RT 由 ① 决定,② 完全异步
}
// 压测对比:同步版 250ms RT → 异步版 60ms RT,吞吐提升 4 倍+(简历亮点)

注意:异步任务失败要补日志、告警和重试(后面第29篇 MQ 削峰会讲更稳的方案:主流程发 MQ,消费者异步处理)。


五、面试话术(背下来)

Q:怎么实现的接口异步化?

「下单主流程只做必要同步步骤(校验、锁库存、落库),把短信、统计、券这类非核心业务放 MQ 或 CompletableFuture+独立线程池异步执行;压测证明 RT 从 250ms 降到 60ms,吞吐提升 4 倍。关键点是异步任务要有失败补偿和监控,不能静默丢业务。」

Q:CompletableFuture 和 Future 区别?

「Future 只有阻塞 get,而且没法组合。CompletableFuture 实现了 CompletionStage,可以 thenApply/thenCombine/allOf 链式编排,异步结果先做回调再组合,还内置异常处理和自定义线程池。」

Q:默认线程池的坑?

「默认 ForkJoinPool.commonPool 是全局共享的,核数-1 个线程,被 parallelStream 和所有 CF 共用;一有 IO 阻塞就互相饿死。所以业务必须显式指定专有线程池并合理配参。」

Q:thenApply 和 thenCompose?

「thenApply 的 fn 返回普通值,结果包在同一个 CF 里;thenCompose 的 fn 返回另一个 CompletableFuture,会拍平成一个 CF,避免嵌套。场景是『上一个异步结果要驱动下一个异步任务』时用 thenCompose。」


六、踩坑总结(易错点)

# 坑 说明
1 get() 不带超时 永久阻塞;一定要 get(timeout) 或 orTimeout
2 用默认 commonPool 全局共享,IO 任务互相拖累
3 thenApply 返回 CF 嵌套 应改用 thenCompose
4 异常没收敛 链式中的异常会「吞掉」直到遇到 exceptionally/handle;不加最终会静默
5 join() 抛 CompletionException 要 getCause() 拿原始异常
6 异步任务没补偿机制 异步失败 = 业务静默丢失(如没发短信、没发券),要加日志/重试/MQ
7 把事务操作放异步线程 事务上下文(@Transactional)不随线程传递,跨线程事务必须自己管理

七、课后作业

  1. 把「订单详情页并行聚合」写成小工程跑起来,对比串行/并行耗时。
  2. 用 supplyAsync + thenCompose 实现「步骤A → 异步步骤B → 异步步骤C」三级依赖链。
  3. 用 exceptionally + handle 分别实现:失败返回默认值 / 失败记录日志后返回默认。
  4. 思考:为什么市面上用「MQ 异步」做削峰多于「CompletableFuture 异步」?(提示:可靠性、削峰填谷、流量控制、可追溯)
相关推荐
paopaokaka_luck2 小时前
流浪动物救助与收养系统(DeepSeek AI对话、 ECharts多维分析、救助流程衔接、领养与回访衔接、 捐赠确认与公示、志愿者协作)
java·前端·javascript·人工智能·mysql·echarts·推荐算法
Thomas.Sir2 小时前
第9课:SpringBoot4.x Web开发基础:WebMvc自动配置、静态资源、请求参数接收
java·spring boot
程序 代码狂人2 小时前
切片表与分区表的区别(数据开发面试版)
java·大数据·开发语言·数据仓库·算法
2501_944521593 小时前
Immer 跨端三方库仓颉适配全流程
python·eclipse·django
信誓旦旦的程序猿3 小时前
【Python量化实战宝典 #26】止损与回撤控制手册:移动止损、最大回撤(Python 可跑)
python·1024程序员节·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
2501_944521593 小时前
Deepmerge 跨端三方库仓颉适配全流程
java·python·eclipse·github
Patrick在香港3 小时前
同一行 strftime,两台机器跑出两种日期
python·datetime·时区·locale·日期处理
2601_967987373 小时前
用Python+OpenCV+YOLO写个游戏脚本:从自动瞄准到自动采集的保姆级教程
python·opencv·yolo·目标检测·游戏自动化
meiying88383 小时前
快递单号批量查询Python脚本:从手动到自动化的完整实现
开发语言·python·自动化