← 返回题目列表

CompletableFuture 是什么?和 Future 相比解决了什么问题?

高频 中等 第 8 / 31 题 更新于 2026/07/26
CompletableFuture异步编程Future线程池

简化版

CompletableFuture 是 Java 8 引入的异步编排工具,解决了老 Future 的两大痛点:① 拿结果只能 get() 阻塞或轮询,没法注册回调;② 多个异步任务没法方便地串联、组合。它用 thenApply/thenCompose/thenCombine/allOf 等方法把「任务完成后做什么」写成链式回调,不阻塞主线程;还能优雅处理异常(exceptionally/handle)。本质是一个「可以被手动完成、且支持回调编排的 Future」。

详细版

老 Future 的问题Future.get() 会阻塞当前线程直到结果就绪,或者你只能 isDone() 轮询——两种都很笨。而且多个 Future 之间无法表达「A 完成后自动触发 B」「等 A、B 都完成再汇总」这类依赖关系。

CompletableFuture 的能力分几类:

// 1. 创建异步任务(默认用 ForkJoinPool.commonPool,建议传自定义线程池)
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> queryUser(), pool);

// 2. 串联:一个完成后接着做(回调,不阻塞)
cf.thenApply(user -> user.getName())        // 转换结果
  .thenAccept(name -> log.info(name))       // 消费结果,无返回
  .thenRun(() -> log.info("done"));         // 不关心结果,只执行

// 3. 组合两个任务
cfA.thenCompose(a -> queryAsync(a));        // A 的结果喂给下一个异步任务(扁平化,避免嵌套 CF)
cfA.thenCombine(cfB, (a, b) -> a + b);      // 等 A、B 都完成,合并两者结果

// 4. 多任务汇聚
CompletableFuture.allOf(cf1, cf2, cf3).join();  // 等全部完成
CompletableFuture.anyOf(cf1, cf2).join();       // 任一完成即返回

// 5. 异常处理
cf.exceptionally(ex -> "默认值")            // 出异常时兜底
  .handle((result, ex) -> ex == null ? result : "兜底");  // 无论成功失败都处理

thenApply vs thenCompose:前者把结果做同步转换(T -> U);后者用于「结果再触发一个异步任务」(T -> CompletableFuture<U>),避免出现 CompletableFuture<CompletableFuture<U>> 的嵌套,相当于 Stream 的 map vs flatMap

带 Async 后缀thenApplyAsync):默认回调在「完成前一个任务的那个线程」执行;带 Async 会把回调重新提交到线程池,适合回调较重、不想占用上游线程时。

⚠️ supplyAsync 不传线程池时用的是 ForkJoinPool.commonPool(),它是全 JVM 共享的、线程数约等于 CPU 核数。做 IO 密集任务时容易被占满拖垮其他任务,生产环境务必传自定义线程池

完整版教学

一、为什么需要它:Future 的异步是「假异步」

Future 表面是异步,实际用起来常退化成同步。看这个场景:查用户、查订单、查库存三个远程调用,本该并行,用 Future 却是:

Future<User> fu = pool.submit(() -> queryUser());
Future<Order> fo = pool.submit(() -> queryOrder());
User user = fu.get();     // 阻塞,主线程在这干等
Order order = fo.get();   // 又阻塞

get() 一调用,主线程就被钉住了。更糟的是「A 的结果作为 B 的入参」这种依赖,Future 只能 get 出 A 再手动提交 B,中间线程全程空等。Future 缺的是「完成后自动触发下一步」的回调能力——CompletableFuture 就是来补这个的:把「然后做什么」注册成回调,任务完成时自动执行,主线程不用守着。

二、三类核心方法:转换、消费、组合

CompletableFuture 的 API 虽多,按「回调有没有入参/返回值」一分就清楚了:

方法族入参返回用途
thenApply(fn)上一步结果新结果转换(map)
thenAccept(fn)上一步结果消费结果
thenRun(fn)只跑动作,不关心结果
thenCompose(fn)上一步结果新的 CF串联异步任务(flatMap)
thenCombine(cf, fn)两个结果合并结果两个独立任务汇合
allOf/anyOf多个 CFVoid/Object多任务汇聚

记忆线索:Apply 有进有出、Accept 有进无出、Run 无进无出;Compose 接异步、Combine 合并俩。掌握这张表,绝大多数编排都能写出来。

三、thenApply vs thenCompose:map 与 flatMap 之别

这是最常考的一对。区别在回调的返回类型

// thenApply:回调返回普通值 T -> U
CompletableFuture<Integer> a = cf.thenApply(user -> user.getAge());   // CF<Integer> ✓

// 若回调本身又返回一个 CF,用 thenApply 会套娃:
cf.thenApply(user -> queryOrderAsync(user));   // 得到 CF<CompletableFuture<Order>> ✗ 嵌套!

// thenCompose:回调返回 CF,自动扁平化 T -> CF<U>
cf.thenCompose(user -> queryOrderAsync(user)); // 得到 CF<Order> ✓ 干净

规则:回调返回的是普通值用 thenApply;回调本身返回一个 CompletableFuture 用 thenCompose。这跟 Optional/Stream 的 map vs flatMap 完全同理——flatMap 负责「拆掉一层包装」。

四、并行编排:把串行 3 秒压成 1 秒

CompletableFuture 真正的威力是让本无依赖的任务并行。假设三个查询各耗时 1 秒:

串行 get():            并行 allOf:
queryUser  [===1s===]   queryUser  [===1s===]
queryOrder     [===1s===]  queryOrder [===1s===]  ← 同时进行
queryStock         [===1s===]  queryStock [===1s===]
总计 ≈ 3 秒              总计 ≈ 1 秒
CompletableFuture<User> cu = CompletableFuture.supplyAsync(() -> queryUser(), pool);
CompletableFuture<Order> co = CompletableFuture.supplyAsync(() -> queryOrder(), pool);
CompletableFuture<Stock> cs = CompletableFuture.supplyAsync(() -> queryStock(), pool);
CompletableFuture.allOf(cu, co, cs).join();   // 等三个都完成,总耗时 ≈ 最慢的那个
Result r = new Result(cu.join(), co.join(), cs.join());  // 此时都已完成,join 不再阻塞

聚合接口把 3 秒压到 1 秒,这是它在高并发后端最典型的价值。

五、异常处理:链式里的 try-catch

同步代码用 try-catch,异步回调链里则用专门的方法。三个关键:

cf.exceptionally(ex -> {        // 只在异常时触发,返回兜底值(相当于 catch)
    log.error("失败", ex);
    return DEFAULT;
});

cf.handle((result, ex) -> {     // 无论成功失败都触发,能拿到结果或异常
    return ex == null ? result : DEFAULT;
});

cf.whenComplete((result, ex) -> {  // 类似 handle 但不改变结果(相当于 finally)
    log.info("完成");
});

关键机制:异常会沿链向下传播——链上任何一步抛异常,后续的 thenApply 等会被跳过,直到遇到 exceptionally/handle 才被捕获处理。所以通常在链尾统一兜底。注意 whenComplete 不吞异常(异常仍会继续往下传),handle 则能「消化」异常返回正常值。

六、线程池陷阱:别用默认的 commonPool

supplyAsync(fn) 不传线程池时,用的是 ForkJoinPool.commonPool(),这有两个坑:

commonPool 特性:
- 全 JVM 共享,所有不传池的 CF 都挤这一个池
- 线程数 = CPU 核数 - 1(如 8 核 → 7 个线程)
- 为 CPU 密集设计
问题:若你在里面做 IO(远程调用、DB 查询),线程会长时间阻塞等 IO,
      7 个线程很快被占满,同一 JVM 里其他用 commonPool 的任务全部饿死

正确做法是按业务隔离线程池:IO 密集任务传一个线程数较多的自定义池,CPU 密集任务用另一个池。这跟「不用 Executors 快捷方法、手动 new ThreadPoolExecutor」是同一个工程原则——异步框架好用,但线程资源必须自己掌控。

记忆钩子:「Future 只能等,CompletableFuture 能编排」;核心记住「thenApply 转换、thenCompose 接异步、thenCombine 合并、allOf 汇聚、exceptionally 兜底」,以及「生产必传自定义线程池」。

七、常见误区与追问

  • 误区:CompletableFuture 一定是异步执行的。 不带 Async 后缀的回调(如 thenApply)可能在完成上一步的那个线程里同步执行,甚至在调用线程里执行;要强制切线程池用带 Async 的版本。
  • 误区:不传线程池也没关系。 默认的 commonPool 是全 JVM 共享、按 CPU 核数配置的,IO 密集任务会占满它拖垮其他任务,生产必须传自定义池。
  • 误区:thenApply 和 thenCompose 可以互换。 回调返回普通值用 thenApply,返回 CF 用 thenCompose,否则会得到嵌套的 CF<CF<T>>
  • 误区:链中间抛异常会导致程序崩溃。 异常会沿链传播并被 exceptionally/handle 捕获;若一直不处理,get() 时会抛 ExecutionException 包裹原异常。
  • 追问:join() 和 get() 有什么区别? 都阻塞等结果,但 get() 抛受检异常(需 try-catch),join() 抛非受检的 CompletionException,写链式代码更顺手。
  • 追问:allOf 返回 CompletableFuture<Void>,怎么拿各任务结果? allOf 只表示「都完成」,结果要各自 join(此时已完成不阻塞);或用 stream 收集各 CF 的 join 值。
  • 追问:whenComplete 和 handle 区别? 都在完成时触发;handle 能返回新值/消化异常(改变结果),whenComplete 只观察不改变结果、且异常继续向下传播(更像 finally)。

八、加强记忆

把 CompletableFuture 记成「会自动接力的 Future」:老 Future 只能 get() 阻塞干等、任务间无法表达依赖,而 CompletableFuture 让你把「完成后做什么」写成回调链,主线程不用守着。核心 API 按「有进有出/有进无出/无进无出」记——thenApply 转换、thenAccept 消费、thenRun 纯动作;接续异步任务用 thenCompose(flatMap,避免套娃)、合并两个任务用 thenCombine、汇聚多个用 allOf/anyOf。它最实的价值是把无依赖的远程调用并行化,串行 3 秒压成 1 秒。异常靠 exceptionally(catch)、handle(catch 且改结果)、whenComplete(finally)沿链兜底。最后钉死一条工程红线:不传线程池会用全局 commonPool,IO 任务会拖垮它,生产环境必须传自定义线程池。一句话「Future 只能等、CompletableFuture 能编排,转换 thenApply、接异步 thenCompose、汇聚 allOf、生产传自定义池」。