CompletableFuture 最佳实践与避坑指南(2025 版)

664 阅读4分钟

作者:一个被 CompletableFuture 虐过千百次的后端老司机

1764122865994.png 如果你在 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 倍)

  1. 复用同一个 CompletableFuture 对象,避免链式创建大量 Stage
  2. 能用 thenCompose 就不要 thenApply + return CompletableFuture
  3. 大批量任务用 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) 而不是自己循环
  4. 避免在 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 坑得最惨的一次经历~

(完)