作者:一个被 CompletableFuture 虐过千百次的后端老司机
如果你在 Java 后端开发中还没被
CompletableFuture 坑过,那只能说明你还没真正用过它。
它确实强大、优雅、完全异步,但也处处是陷阱,一不小心就会出现线程堵死、异常吞掉、内存泄漏、性能雪崩。
本文总结了 2024~2025 年我在高并发系统(日 PV 10 亿+)中踩过的所有坑,强烈建议收藏。
一、永远不要这样做(血的教训)
1. 在默认 ForkJoinPool.commonPool() 上跑阻塞任务
// 绝对禁止!!
CompletableFuture.supplyAsync(() -> {
httpClient.blockingCall(); // 阻塞整个 commonPool
return result;
});
ForkJoinPool.commonPool() 是 JDK 全局公用的,默认线程数只有 CPU 核数-1(或者最大 32767)。一旦你把阻塞 IO(MySQL、Redis、HTTP、gRPC)扔进去,几百个请求就能把整个 JVM 的异步线程耗尽,导致所有 CompletableFuture 卡死。
正确做法:自定义线程池
public class AsyncExecutor {
// CPU 密集型用默认 commonPool 就行
public static final Executor CPU_BOUND = ForkJoinPool.commonPool();
// IO 密集型必须单独建池,线程数建议 50~200(根据业务压测)
public static final Executor IO_BOUND = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 10,
new ThreadFactoryBuilder().setNamePrefix("async-io-").build()
);
}
// 使用
CompletableFuture.supplyAsync(() -> blockingHttpCall(), AsyncExecutor.IO_BOUND);
2. 直接使用 .get() / .join()
// 看起来很方便,实际上把异步变成同步
String result = future.get(); // 阻塞当前线程
String result = future.join(); // 吞异常!!
.join() 的最大问题是:它把 checked Exception 包装成 CompletionException 再包装成 unchecked,异常栈被破坏,线上排查极难。
正确做法:全部用组合式 API,不要阻塞
future.exceptionally(ex -> {
log.error("异步任务失败", ex);
return fallbackValue;
});
3. 异常被默默吞掉
CompletableFuture.failedFuture(new RuntimeException("boom"))
.thenApply(r -> r + 1); // 这一步根本不会执行,但你看不出来
很多组合操作(thenApply、thenAccept、thenRun)在上游异常时直接跳过,不会抛异常,也不会日志。
最佳实践:每条链路最后加 handle 或 whenComplete
future.handle((result, ex) -> {
if (ex != null) {
log.error("异步任务异常", ex);
return fallback;
}
return result;
});
4. 线程池来不及回收导致 OOM
// 每次都 new ThreadPoolExecutor,永远不会 shutdown
CompletableFuture.supplyAsync(() -> doSomething(),
new ThreadPoolExecutor(...));
每次 new 出一个线程池,线程永远不会退出,内存泄漏。
正确做法:全局统一管理线程池(Spring 项目用 @Bean,普通项目用静态 final)
二、2025 年推荐的终极写法
1. 基础模板(强烈建议复制粘贴)
public <T> CompletableFuture<T> runAsync(Supplier<T> supplier) {
return CompletableFuture.supplyAsync(supplier, AsyncExecutor.IO_BOUND)
.handle((result, ex) -> {
if (ex != null) {
// 统一异常处理 + 日志 + 链路追踪
log.error("Async task failed, traceId={}", MDC.get("traceId"), ex);
Sentry.captureException(ex);
return fallbackValue(supplier); // 业务降级
}
return result;
});
}
2. 并行多个任务(AllOf 正确姿势)
public CompletableFuture<UserProfile> getUserProfile(Long userId) {
CompletableFuture<UserBase> baseFuture = runAsync(() -> userService.getBase(userId));
CompletableFuture<List<Order>> orderFuture = runAsync(() -> orderService.last10(userId));
CompletableFuture<List<Post>> postFuture = runAsync(() -> postService.last20(userId));
CompletableFuture<UserRisk> riskFuture = runAsync(() -> riskService.check(userId));
return CompletableFuture.allOf(baseFuture, orderFuture, postFuture, riskFuture)
.thenApply(v -> {
// 这里一定要 getNow(null) 而不是 join()!
UserBase base = baseFuture.getNow(null);
List<Order> orders = orderFuture.getNow(Collections.emptyList());
List<Post> posts = postFuture.getNow(Collections.emptyList());
UserRisk risk = riskFuture.getNow(UserRisk.NORMAL);
return UserProfile.assemble(base, orders, posts, risk);
});
}
关键点:
- 用
getNow(defaultValue)而不是.join(),避免异常被吞 allOf完成后任一失败整个都是失败状态,必须用handle包装
3. 超时控制(JDK 8/11 必须自己实现)
public static <T> CompletableFuture<T> withTimeout(CompletableFuture<T> future, long timeout, TimeUnit unit) {
CompletableFuture<T> timeoutFuture = new CompletableFuture<>();
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.schedule(() -> timeoutFuture.completeExceptionally(new TimeoutException("Operation timed out")),
timeout, unit);
scheduler.shutdown();
return future.applyToEither(timeoutFuture, Function.identity());
}
// 使用
CompletableFuture<User> future = getUserAsync(userId)
.withTimeout(300, TimeUnit.MILLISECONDS)
.exceptionally(ex -> {
if (ex instanceof TimeoutException) {
metrics.counter("user.timeout").increment();
}
return User.EMPTY;
});
JDK 19+ 可以直接用:
future.orTimeout(300, TimeUnit.MILLISECONDS)
.exceptionally(ex -> { ... })
4. 限流 + 熔断(配合 Resilience4j)
Bulkhead bulkhead = Bulkhead.of("user-service", BulkheadConfig.custom()
.maxConcurrentCalls(50)
.maxWaitDuration(Duration.ofMillis(100))
.build());
CompletableFuture.supplyAsync(() -> Bulkhead.decorateSupplier(bulkhead, () -> callRemote())::get)
.thenCompose(Function.identity());
三、性能优化技巧(实测提升 3~5 倍)
- 复用同一个 CompletableFuture 对象,避免链式创建大量 Stage
- 能用 thenCompose 就不要 thenApply + return CompletableFuture
- 大批量任务用
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))而不是自己循环 - 避免在 lambda 里捕获大对象(可能导致内存泄漏)
// 坏味道:捕获了整个 Request 对象
list.stream().map(item -> CompletableFuture.supplyAsync(() -> process(item, request)));
// 推荐:只传必要字段
String traceId = request.getTraceId();
list.stream().map(item -> CompletableFuture.supplyAsync(() -> process(item, traceId)));
四、排查问题必备工具
// 打印线程池状态(线上应急必备)
ThreadPoolExecutor executor = (ThreadPoolExecutor) AsyncExecutor.IO_BOUND;
log.info("poolSize={} active={} queue={} taskCount={}",
executor.getPoolSize(),
executor.getActiveCount(),
executor.getQueue().size(),
executor.getTaskCount());
或者直接用 Arthas:
thread -n 3 | grep async-io
dashboard
五、总结:2025 年的最佳实践清单
- 所有异步任务使用自定义 IO 线程池,绝不用 commonPool 做阻塞
- 每条链路最后必须有 handle / whenComplete 统一处理异常
- 绝不使用 .get() / .join()(除非在 main 方法测试)
- 并行任务用 allOf + getNow(default) 组合结果
- 必须加超时(orTimeout / completeOnTimeout)
- 大并发场景配合 Bulkhead / RateLimiter
- 全局统一线程池 + 监控队列长度
把这篇文章保存下来,下次再有人跟你说 “CompletableFuture 很简单啊”,把链接甩给他。
欢迎留言讨论你被 CompletableFuture 坑得最惨的一次经历~
(完)