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 | 多个 CF | Void/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、生产传自定义池」。