【Java项目技术亮点】CompletableFuture异步编排

写在前面

我见过太多人的异步代码写得像屎一样------要么到处都是 thread.start(),要么用 CountDownLatch 嵌套了五六层,看都看不懂。其实 Java 8 的 CompletableFuture 早就把这些痛点解决了,链式调用、组合编排、异常处理一应俱全。这篇文章我把核心 API、实战案例和面试常考的坑都过了一遍,建议收藏。

文章目录

    • 一、为什么需要异步编排?
      • [1.1 一个真实的性能瓶颈](#1.1 一个真实的性能瓶颈)
      • [1.2 时间线对比](#1.2 时间线对比)
      • [1.3 生活类比:做饭](#1.3 生活类比:做饭)
    • [二、CompletableFuture 核心 API](#二、CompletableFuture 核心 API)
      • [2.1 创建异步任务](#2.1 创建异步任务)
      • [2.2 链式转换](#2.2 链式转换)
      • [2.3 组合多个异步任务](#2.3 组合多个异步任务)
      • [2.4 异常处理](#2.4 异常处理)
    • 三、实战:首页数据聚合
      • [3.1 串行实现(慢)](#3.1 串行实现(慢))
      • [3.2 CompletableFuture 并行实现(快)](#3.2 CompletableFuture 并行实现(快))
      • [3.3 为什么不用默认的 ForkJoinPool?](#3.3 为什么不用默认的 ForkJoinPool?)
      • [3.4 超时控制](#3.4 超时控制)
    • [四、CompletableFuture 执行流程图](#四、CompletableFuture 执行流程图)
    • 五、踩坑指南
      • [5.1 不要用默认的 ForkJoinPool](#5.1 不要用默认的 ForkJoinPool)
      • [5.2 allOf 返回 CompletableFuture<Void>](#5.2 allOf 返回 CompletableFuture)
      • [5.3 异常被吞没的问题](#5.3 异常被吞没的问题)
      • [5.4 ThreadLocal 上下文丢失](#5.4 ThreadLocal 上下文丢失)
    • 六、问题与解答
      • [Q1: supplyAsync 和 runAsync 的区别?](#Q1: supplyAsync 和 runAsync 的区别?)
      • [Q2: thenApply 和 thenCompose 的区别?](#Q2: thenApply 和 thenCompose 的区别?)
      • [Q3: 如何设置全局异常处理?](#Q3: 如何设置全局异常处理?)
    • 七、面试高频考点汇总
      • [考点1:CompletableFuture 的核心优势是什么?](#考点1:CompletableFuture 的核心优势是什么?)
      • [考点2:CompletableFuture 的 thenApply 和 thenCompose 有什么区别?](#考点2:CompletableFuture 的 thenApply 和 thenCompose 有什么区别?)
      • [考点3:为什么不要用 ForkJoinPool.commonPool()?](#考点3:为什么不要用 ForkJoinPool.commonPool()?)
      • [考点4:CompletableFuture 的异常处理有哪些方式?](#考点4:CompletableFuture 的异常处理有哪些方式?)
      • [考点5:CompletableFuture 在什么场景下不适用?](#考点5:CompletableFuture 在什么场景下不适用?)
    • 八、模拟面试官提问
    • 九、互动话题
    • 参考资料

一、为什么需要异步编排?

1.1 一个真实的性能瓶颈

假设我们有一个首页聚合接口,需要调用 4 个服务获取数据:

服务 平均耗时
用户服务(获取用户信息) 200ms
推荐服务(获取推荐列表) 800ms
消息服务(获取未读消息数) 500ms
配置服务(获取运营配置) 500ms

串行调用 :200 + 800 + 500 + 500 = 2000ms

并行调用 :max(200, 800, 500, 500) = 800ms

差了 2.5 倍!在高并发场景下,这个差距会直接影响系统的吞吐量和用户体验。

1.2 时间线对比

串行调用时间线

复制代码
0ms -------- 200ms --------- 1000ms --------- 1500ms --------- 2000ms
|--- 用户服务 ---|--- 推荐服务 ---|--- 消息服务 ---|--- 配置服务 ---|

并行调用时间线

复制代码
0ms ---- 200ms
|--- 用户服务 ---|

0ms ---------------- 800ms
|------ 推荐服务 ------|

0ms ---------- 500ms
|---- 消息服务 ----|

0ms ---------- 500ms
|---- 配置服务 ----|

总耗时 = max(200, 800, 500, 500) = 800ms

1.3 生活类比:做饭

做饭就是最好的异步编排例子:

  • 串行 = 先洗菜,洗完切菜,切完炒菜,炒完煲汤。一道一道来,一桌菜做下来天都黑了
  • 并行 = 电饭煲煮饭的同时,你切菜、炒菜、煲汤一起干。效率直接翻倍

CompletableFuture 就是帮你管理这些"同时干的活"的工具。


二、CompletableFuture 核心 API

2.1 创建异步任务

supplyAsync:有返回值的异步任务

java 复制代码
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class SupplyAsyncDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // supplyAsync:有返回值
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("线程: " + Thread.currentThread().getName());
            return "Hello, CompletableFuture!";
        });

        // 获取结果(会阻塞直到任务完成)
        String result = future.get();
        System.out.println("结果: " + result);
        // 输出: 结果: Hello, CompletableFuture!
    }
}

runAsync:没有返回值的异步任务

java 复制代码
public class RunAsyncDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // runAsync:无返回值
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            System.out.println("执行异步任务,线程: " + Thread.currentThread().getName());
            // 这里没有return
        });

        future.get(); // 等待任务完成
        System.out.println("任务执行完毕");
    }
}

两者的区别supplyAsync 有返回值(CompletableFuture<T>),runAsync 没有返回值(CompletableFuture<Void>)。需要结果的用 supplyAsync,只执行副作用用 runAsync

2.2 链式转换

thenApply:接收上一个任务的结果,返回一个新的结果

java 复制代码
public class ThenApplyDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> 100)
                // 将 Integer 转换为 String
                .thenApply(num -> "数字是: " + num)
                // 将 String 转换为大写
                .thenApply(str -> str.toUpperCase());

        System.out.println(future.get());
        // 输出: 数字是: 100
    }
}

thenAccept:接收上一个任务的结果,消费它,不返回新结果

java 复制代码
public class ThenAcceptDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture.supplyAsync(() -> "Hello")
                .thenAccept(str -> {
                    // 消费结果,打印出来
                    System.out.println("接收到: " + str);
                });

        // 这里返回的是 CompletableFuture<Void>
    }
}

thenRun:不接收上一个任务的结果,直接执行一段逻辑

java 复制代码
public class ThenRunDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture.supplyAsync(() -> {
                    System.out.println("第一步:获取数据");
                    return "data";
                })
                .thenRun(() -> {
                    // 不关心上一步的结果,直接执行
                    System.out.println("第二步:记录日志");
                });

        CompletableFuture.allOf().join(); // 等待完成
    }
}

2.3 组合多个异步任务

thenCombine:合并两个任务的结果

java 复制代码
public class ThenCombineDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> " World");

        // thenCombine:合并两个结果
        CompletableFuture<String> combined = future1.thenCombine(future2, (s1, s2) -> s1 + s2);

        System.out.println(combined.get());
        // 输出: Hello World
    }
}

thenCompose:扁平化映射(类似 Stream 的 flatMap)

java 复制代码
public class ThenComposeDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // thenCompose:当返回值本身也是 CompletableFuture 时使用
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> 42)
                .thenCompose(num -> CompletableFuture.supplyAsync(() -> "数字是: " + num));

        System.out.println(future.get());
        // 输出: 数字是: 42
    }
}

thenApply vs thenCompose 的区别thenApply 返回的是普通值,thenCompose 返回的是 CompletableFuture。如果回调方法返回的已经是 CompletableFuture,就用 thenCompose 来避免嵌套。

allOf:等待所有任务完成

java 复制代码
public class AllOfDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
            sleep(1000);
            return "任务1完成";
        });
        CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
            sleep(2000);
            return "任务2完成";
        });
        CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> {
            sleep(1500);
            return "任务3完成";
        });

        // allOf:等待所有任务完成,返回 CompletableFuture<Void>
        CompletableFuture<Void> allFutures = CompletableFuture.allOf(f1, f2, f3);

        // 需要手动收集结果
        allFutures.join();

        System.out.println(f1.join()); // 任务1完成
        System.out.println(f2.join()); // 任务2完成
        System.out.println(f3.join()); // 任务3完成
    }

    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

anyOf:任意一个任务完成就返回

java 复制代码
public class AnyOfDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
            sleep(3000);
            return "慢任务";
        });
        CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
            sleep(1000);
            return "快任务";
        });

        // anyOf:哪个先完成就返回哪个
        CompletableFuture<Object> anyFuture = CompletableFuture.anyOf(f1, f2);

        System.out.println(anyFuture.get());
        // 输出: 快任务(因为f2先完成)
    }
}

2.4 异常处理

exceptionally:捕获异常并返回默认值

java 复制代码
public class ExceptionallyDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            if (true) {
                throw new RuntimeException("出错了!");
            }
            return 100;
        }).exceptionally(ex -> {
            System.out.println("捕获异常: " + ex.getMessage());
            return -1; // 返回默认值
        });

        System.out.println(future.get());
        // 输出: 捕获异常: java.lang.RuntimeException: 出错了!
        // 输出: -1
    }
}

handle:同时处理正常结果和异常

java 复制代码
public class HandleDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            if (Math.random() > 0.5) {
                throw new RuntimeException("随机出错了!");
            }
            return 100;
        }).handle((result, ex) -> {
            if (ex != null) {
                System.out.println("异常了: " + ex.getMessage());
                return 0;
            }
            System.out.println("正常结果: " + result);
            return result * 2;
        });

        System.out.println(future.get());
    }
}

whenComplete:类似 handle,但不改变结果

java 复制代码
public class WhenCompleteDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 100)
                .whenComplete((result, ex) -> {
                    if (ex != null) {
                        System.out.println("任务异常: " + ex.getMessage());
                    } else {
                        System.out.println("任务完成,结果: " + result);
                    }
                    // 注意:这里不能改变返回值
                });

        System.out.println(future.get());
        // 输出: 任务完成,结果: 100
        // 输出: 100
    }
}

三、实战:首页数据聚合

3.1 串行实现(慢)

java 复制代码
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 首页数据聚合 - 串行实现
 */
public class HomePageSerialDemo {

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

        // 串行调用,一个接一个
        UserInfo userInfo = getUserInfo(1001L);
        List<Product> recommendations = getRecommendations(1001L);
        int unreadCount = getUnreadMessages(1001L);
        AppConfig config = getAppConfig();

        long end = System.currentTimeMillis();
        System.out.println("串行总耗时: " + (end - start) + "ms");
        // 预期: ~2000ms
    }

    // 模拟用户服务调用(200ms)
    static UserInfo getUserInfo(Long userId) {
        sleep(200);
        return new UserInfo(userId, "张三");
    }

    // 模拟推荐服务调用(800ms)
    static List<Product> getRecommendations(Long userId) {
        sleep(800);
        return java.util.Collections.emptyList();
    }

    // 模拟消息服务调用(500ms)
    static int getUnreadMessages(Long userId) {
        sleep(500);
        return 5;
    }

    // 模拟配置服务调用(500ms)
    static AppConfig getAppConfig() {
        sleep(500);
        return new AppConfig(true, "v2.0");
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { e.printStackTrace(); }
    }

    // 简单的数据类
    static class UserInfo {
        Long id; String name;
        UserInfo(Long id, String name) { this.id = id; this.name = name; }
    }
    static class Product {}
    static class AppConfig {
        boolean showBanner; String version;
        AppConfig(boolean b, String v) { this.showBanner = b; this.version = v; }
    }
}

3.2 CompletableFuture 并行实现(快)

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

/**
 * 首页数据聚合 - CompletableFuture并行实现
 */
public class HomePageParallelDemo {

    // 自定义线程池(重要!不要用 ForkJoinPool.commonPool())
    private static final ExecutorService IO_POOL = new ThreadPoolExecutor(
            20,                     // 核心线程数
            50,                     // 最大线程数
            60L, TimeUnit.SECONDS,  // 空闲线程存活时间
            new LinkedBlockingQueue<>(1000), // 任务队列
            new ThreadFactoryBuilder().setNameFormat("async-pool-%d").build(),
            new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
    );

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();

        Long userId = 1001L;

        // 并行调用4个服务
        CompletableFuture<UserInfo> userFuture = CompletableFuture.supplyAsync(
                () -> getUserInfo(userId), IO_POOL);

        CompletableFuture<List<Product>> recommendFuture = CompletableFuture.supplyAsync(
                () -> getRecommendations(userId), IO_POOL);

        CompletableFuture<Integer> msgFuture = CompletableFuture.supplyAsync(
                () -> getUnreadMessages(userId), IO_POOL);

        CompletableFuture<AppConfig> configFuture = CompletableFuture.supplyAsync(
                () -> getAppConfig(), IO_POOL);

        // 等待所有任务完成
        CompletableFuture.allOf(userFuture, recommendFuture, msgFuture, configFuture).join();

        // 收集结果
        HomePageData data = new HomePageData(
                userFuture.get(),
                recommendFuture.get(),
                msgFuture.get(),
                configFuture.get()
        );

        long end = System.currentTimeMillis();
        System.out.println("并行总耗时: " + (end - start) + "ms");
        // 预期: ~800ms(取决于最慢的服务)
        System.out.println("用户: " + data.userInfo.name);
        System.out.println("未读消息: " + data.unreadCount);

        IO_POOL.shutdown();
    }

    // ... 同上,省略模拟方法和数据类
    static UserInfo getUserInfo(Long userId) { sleep(200); return new UserInfo(userId, "张三"); }
    static List<Product> getRecommendations(Long userId) { sleep(800); return java.util.Collections.emptyList(); }
    static int getUnreadMessages(Long userId) { sleep(500); return 5; }
    static AppConfig getAppConfig() { sleep(500); return new AppConfig(true, "v2.0"); }
    static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { e.printStackTrace(); } }

    static class UserInfo { Long id; String name; UserInfo(Long id, String name) { this.id = id; this.name = name; } }
    static class Product {}
    static class AppConfig { boolean showBanner; String version; AppConfig(boolean b, String v) { this.showBanner = b; this.version = v; } }
    static class HomePageData {
        UserInfo userInfo; List<Product> recommendations; int unreadCount; AppConfig config;
        HomePageData(UserInfo u, List<Product> r, int m, AppConfig c) {
            this.userInfo = u; this.recommendations = r; this.unreadCount = m; this.config = c;
        }
    }

    // 简单的线程工厂(实际项目建议用 Guava 的 ThreadFactoryBuilder)
    static class ThreadFactoryBuilder {
        private String nameFormat = "pool-%d";
        ThreadFactoryBuilder setNameFormat(String format) { this.nameFormat = format; return this; }
        java.util.concurrent.ThreadFactory build() {
            return r -> {
                Thread t = new Thread(r);
                t.setName(String.format(nameFormat, r.hashCode()));
                t.setDaemon(true);
                return t;
            };
        }
    }
}

3.3 为什么不用默认的 ForkJoinPool?

CompletableFuture.supplyAsync() 如果不指定线程池,默认使用 ForkJoinPool.commonPool()。这个池子有几个问题:

  1. 线程数 = CPU 核心数 - 1:在 4 核机器上只有 3 个线程,IO 密集型任务根本不够用
  2. 全局共享 :所有 CompletableFuture 共享这个池子,一个慢任务会拖累所有异步任务
  3. 无法监控和调优:你没法单独对这个池子做监控、动态调整线程数

正确做法:为不同类型的任务创建独立的线程池。

3.4 超时控制

Java 9+ 提供了超时控制 API:

java 复制代码
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class TimeoutDemo {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            sleep(5000); // 模拟耗时5秒的任务
            return "结果";
        });

        // 方式1:超时后返回默认值(Java 9+)
        CompletableFuture<String> withTimeout = future
                .completeOnTimeout("超时默认值", 2, TimeUnit.SECONDS);

        // 方式2:超时后抛出异常(Java 9+)
        CompletableFuture<String> withException = future
                .orTimeout(2, TimeUnit.SECONDS)
                .exceptionally(ex -> {
                    if (ex.getCause() instanceof TimeoutException) {
                        System.out.println("任务超时了!");
                    }
                    return "降级数据";
                });

        System.out.println(withTimeout.join());
        // 输出: 超时默认值
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

踩坑提醒

如果你用的是 Java 8,没有 completeOnTimeoutorTimeout,可以用 future.get(2, TimeUnit.SECONDS) 配合 try-catch 来实现超时,或者自己封装一个工具方法。


四、CompletableFuture 执行流程图

下面用一个 DAG 图来描述首页数据聚合的任务编排流程:

复制代码
                    ┌─────────────────┐
                    │   main 线程启动   │
                    └────────┬────────┘
                             │
              ┌──────────────┼──────────────┐
              │              │              │
              ▼              ▼              ▼
     ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
     │ 用户服务    │ │ 推荐服务    │ │ 消息服务    │ │ 配置服务    │
     │ (200ms)    │ │ (800ms)    │ │ (500ms)    │ │ (500ms)    │
     └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘
           │              │              │              │
           └──────────────┼──────────────┼──────────────┘
                          │              │
                          ▼              ▼
                  ┌─────────────────────────┐
                  │    allOf:等待全部完成     │
                  └───────────┬─────────────┘
                              │
                              ▼
                  ┌─────────────────────────┐
                  │  收集结果,组装HomePageData │
                  └───────────┬─────────────┘
                              │
                              ▼
                  ┌─────────────────────────┐
                  │      返回响应给前端       │
                  └─────────────────────────┘

关键点

  • 4 个服务调用是并行的,同时开始
  • allOf 相当于 DAG 中的汇聚节点,等待所有上游任务完成
  • 最终的结果收集 必须在 allOf.join() 之后执行

五、踩坑指南

5.1 不要用默认的 ForkJoinPool

这个坑我踩过。线上有一次接口突然变慢,排查发现是 ForkJoinPool.commonPool() 被一个耗时的异步任务占满了,导致所有使用默认池的 CompletableFuture 都在排队。

java 复制代码
// ❌ 错误:使用默认线程池
CompletableFuture.supplyAsync(() -> callRemoteService());

// ✅ 正确:使用自定义线程池
CompletableFuture.supplyAsync(() -> callRemoteService(), myThreadPool);

5.2 allOf 返回 CompletableFuture

allOf 返回的是 CompletableFuture<Void>不会自动帮你收集结果。你需要手动从原始的 Future 中获取:

java 复制代码
// ❌ 错误写法:allOf的结果是Void,get不到数据
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
all.join();
// all.get() 返回 null,不是你想要的结果

// ✅ 正确写法:手动收集结果
CompletableFuture.allOf(f1, f2, f3).join();
String r1 = f1.join();
String r2 = f2.join();
String r3 = f3.join();

// ✅ 更优雅的写法:封装工具方法
public static <T> CompletableFuture<List<T>> allOf(
        List<CompletableFuture<T>> futures) {
    CompletableFuture<Void> allDone = CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0]));
    return allDone.thenApply(v ->
            futures.stream()
                   .map(CompletableFuture::join)
                   .collect(Collectors.toList())
    );
}

5.3 异常被吞没的问题

CompletableFuture 的异常不会自动抛到主线程,如果你不处理,异常会被"吞掉",你根本不知道任务失败了。

java 复制代码
// ❌ 异常被吞没,没有任何输出
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("悄悄地失败");
}).thenAccept(result -> System.out.println(result));

Thread.sleep(1000);

// ✅ 加上异常处理
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("失败会被捕获");
}).whenComplete((result, ex) -> {
    if (ex != null) {
        System.out.println("任务失败: " + ex.getMessage());
    }
});

5.4 ThreadLocal 上下文丢失

异步线程不是请求线程,ThreadLocal 中的数据(如用户上下文、TraceId)不会自动传递。

java 复制代码
// ❌ 异步线程中拿不到主线程的ThreadLocal
public void handleRequest() {
    UserContext.set(new UserContext(1001L, "admin"));
    CompletableFuture.supplyAsync(() -> {
        // 这里 UserContext.get() 返回 null!
        System.out.println(UserContext.get()); // null
        return "result";
    });
}

// ✅ 手动传递上下文
public void handleRequest() {
    UserContext context = UserContext.get(); // 在主线程中获取
    CompletableFuture.supplyAsync(() -> {
        UserContext.set(context); // 在异步线程中设置
        try {
            System.out.println(UserContext.get()); // 正常获取
            return "result";
        } finally {
            UserContext.clear(); // 记得清理
        }
    }, myThreadPool);
}

踩坑提醒

如果项目用了 InheritableThreadLocal,在 CompletableFuture 中也不靠谱 ,因为线程池中的线程是复用的,不会每次都从父线程继承。推荐使用阿里巴巴的 TransmittableThreadLocal(TTL)来解决这个问题。


六、问题与解答

Q1: supplyAsync 和 runAsync 的区别?

特性 supplyAsync runAsync
返回值 有(CompletableFuture<T> 无(CompletableFuture<Void>
使用场景 需要获取异步任务的结果 只需要执行副作用(如发通知、写日志)
方法签名 Supplier<T> Runnable

简单记:supply = 供应(有产出),run = 跑(只执行)

Q2: thenApply 和 thenCompose 的区别?

这个坑我踩过,当时写代码死活编译不过,最后才发现用错了方法。

  • thenApply :回调返回普通值 ,相当于 Stream.map()
  • thenCompose :回调返回 CompletableFuture ,相当于 Stream.flatMap()
java 复制代码
// thenApply:回调返回普通值
CompletableFuture<Integer> f1 = supplyAsync(() -> 1)
        .thenApply(x -> x + 1);  // 返回 int,自动包装为 CompletableFuture

// thenCompose:回调返回 CompletableFuture
CompletableFuture<Integer> f2 = supplyAsync(() -> 1)
        .thenCompose(x -> supplyAsync(() -> x + 1));  // 返回的是 CompletableFuture<Integer>

什么时候用哪个 :如果回调方法本身返回的是 CompletableFuture,就用 thenCompose,否则用 thenApply

Q3: 如何设置全局异常处理?

CompletableFuture 没有全局异常处理器,但你可以封装一个工具类:

java 复制代码
public class FutureUtils {

    /**
     * 带全局异常处理的 supplyAsync
     */
    public static <T> CompletableFuture<T> safeSupplyAsync(
            Supplier<T> supplier, Executor executor) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                return supplier.get();
            } catch (Exception e) {
                // 全局异常处理:记录日志、上报监控
                log.error("异步任务异常", e);
                throw e;
            }
        }, executor).exceptionally(ex -> {
            log.error("异步任务异常(exceptionally)", ex);
            return null; // 或返回默认值
        });
    }
}

七、面试高频考点汇总

考点1:CompletableFuture 的核心优势是什么?

参考答案 :核心优势是异步编排能力 。通过链式 API 可以方便地组合多个异步任务:串行(thenApply)、并行(allOf)、选择(anyOf)、合并(thenCombine),并且提供了完善的异常处理机制(exceptionallyhandle)。相比传统的 Future,它解决了两个痛点:1)Future.get() 会阻塞;2)无法链式组合多个异步操作。

考点2:CompletableFuture 的 thenApply 和 thenCompose 有什么区别?

参考答案thenApply 类似 Stream.map(),回调函数返回普通值;thenCompose 类似 Stream.flatMap(),回调函数返回 CompletableFuture。如果回调方法本身返回的是 CompletableFuture,必须用 thenCompose 来避免嵌套。用错了会导致编译错误或类型不匹配。

考点3:为什么不要用 ForkJoinPool.commonPool()?

参考答案 :三个原因:1)线程数 = CPU 核心数 - 1,IO 密集型任务不够用;2)全局共享,一个慢任务会拖累所有异步任务;3)无法单独监控和调优。正确做法是为不同类型的异步任务创建独立的线程池,做到线程池隔离

考点4:CompletableFuture 的异常处理有哪些方式?

参考答案 :三种方式:1)exceptionally:捕获异常并返回默认值,类似 try-catch 的 catch;2)handle:同时处理正常结果和异常,不改变返回类型;3)whenComplete:类似 handle,但不能改变返回值,适合做日志记录。建议用 handleexceptionally 来保证异常不会丢失。

考点5:CompletableFuture 在什么场景下不适用?

参考答案 :以下场景不适用:1)任务之间有严格的顺序依赖且不适合并行;2)任务非常轻量(线程切换开销 > 任务执行开销);3)需要精确控制线程执行顺序的场景(考虑用 ExecutorServicesubmit + Future);4)对延迟极度敏感的场景(线程池满时任务会排队)。


八、模拟面试官提问

场景题1:设计一个商品详情页数据聚合接口

面试官:商品详情页需要展示商品基本信息、价格、库存、评价摘要、推荐商品,你怎么设计这个接口?

参考答案

java 复制代码
public ProductDetailVO getProductDetail(Long productId) {
    // 并行调用5个服务
    CompletableFuture<ProductInfo> infoFuture = CompletableFuture.supplyAsync(
            () -> productInfoService.getById(productId), productPool);

    CompletableFuture<PriceInfo> priceFuture = CompletableFuture.supplyAsync(
            () -> priceService.getCurrentPrice(productId), pricePool);

    CompletableFuture<StockInfo> stockFuture = CompletableFuture.supplyAsync(
            () -> stockService.getStock(productId), stockPool);

    CompletableFuture<ReviewSummary> reviewFuture = CompletableFuture.supplyAsync(
            () -> reviewService.getSummary(productId), reviewPool);

    CompletableFuture<List<ProductInfo>> recommendFuture = CompletableFuture.supplyAsync(
            () -> recommendService.getRelated(productId), recommendPool);

    // 等待全部完成,超时3秒
    try {
        CompletableFuture.allOf(infoFuture, priceFuture, stockFuture,
                reviewFuture, recommendFuture)
                .get(3, TimeUnit.SECONDS);
    } catch (TimeoutException e) {
        log.warn("商品详情聚合超时, productId={}", productId);
    }

    // 组装结果(部分数据可能为null,前端需要兜底)
    ProductDetailVO vo = new ProductDetailVO();
    vo.setProductInfo(getSafe(infoFuture));     // 非核心,允许null
    vo.setPrice(getSafe(priceFuture));          // 核心,降级显示"暂无价格"
    vo.setStock(getSafe(stockFuture));          // 核心,降级显示"到货通知"
    vo.setReviewSummary(getSafe(reviewFuture)); // 非核心,允许null
    vo.setRecommendations(getSafe(recommendFuture)); // 非核心,允许null
    return vo;
}

private static <T> T getSafe(CompletableFuture<T> future) {
    try {
        return future.getNow(null); // 不阻塞,已完成返回结果,否则返回null
    } catch (Exception e) {
        return null;
    }
}

设计要点

  • 按服务类型做线程池隔离
  • 核心 vs 非核心数据差异化降级
  • 设置全局超时,防止拖慢整个接口

场景题2:异步任务超时处理方案

面试官:如果某个下游服务超时了,你怎么处理?直接报错还是降级?

参考答案

分三种情况处理:

java 复制代码
public CompletableFuture<String> callWithTimeout(String serviceUrl) {
    CompletableFuture<String> primary = CompletableFuture.supplyAsync(
            () -> httpClient.get(serviceUrl), ioPool);

    // 1. 设置超时(Java 9+)
    primary.orTimeout(2, TimeUnit.SECONDS);

    // 2. 超时后降级到缓存
    return primary.exceptionally(ex -> {
        if (ex.getCause() instanceof TimeoutException) {
            log.warn("服务超时,降级到缓存: {}", serviceUrl);
            return cacheService.get(serviceUrl); // 从缓存获取
        }
        log.error("服务异常: {}", serviceUrl, ex);
        return getDefaultResponse(); // 返回默认值
    });
}

降级策略

  • 核心数据(价格、库存):降级到 Redis 缓存,缓存也没有就返回兜底值
  • 非核心数据(推荐、评价):直接返回空列表,前端不展示即可
  • 记录降级日志,触发监控告警

场景题3:多个微服务并行调用 + 容错

面试官:订单详情页需要调用订单服务、用户服务、支付服务、物流服务,任何一个服务挂了都不能影响其他数据的展示,你怎么设计?

参考答案

java 复制代码
public OrderDetailVO getOrderDetail(String orderId) {
    // 每个服务调用都独立做异常处理
    CompletableFuture<OrderInfo> orderFuture = safeCall(
            () -> orderService.getOrder(orderId),
            "订单服务", new OrderInfo());

    CompletableFuture<UserInfo> userFuture = safeCall(
            () -> userService.getUser(orderId),
            "用户服务", new UserInfo());

    CompletableFuture<PayInfo> payFuture = safeCall(
            () -> payService.getPayInfo(orderId),
            "支付服务", new PayInfo());

    CompletableFuture<LogisticsInfo> logisticsFuture = safeCall(
            () -> logisticsService.getLogistics(orderId),
            "物流服务", new LogisticsInfo());

    // 等待全部完成
    CompletableFuture.allOf(orderFuture, userFuture, payFuture, logisticsFuture).join();

    return new OrderDetailVO(
            orderFuture.join(),
            userFuture.join(),
            payFuture.join(),
            logisticsFuture.join()
    );
}

/**
 * 安全调用:异常不影响其他任务,返回默认值
 */
private <T> CompletableFuture<T> safeCall(
        Supplier<T> supplier, String serviceName, T defaultValue) {
    return CompletableFuture.supplyAsync(() -> {
        try {
            return supplier.get();
        } catch (Exception e) {
            log.error("{}调用失败,使用默认值", serviceName, e);
            return defaultValue;
        }
    }, ioPool);
}

核心思路 :每个服务调用用 safeCall 包装,异常被捕获并返回默认值,不会影响其他任务的执行。

场景题4:CompletableFuture 与 Spring @Async 对比

面试官:你们项目里异步任务用的是 CompletableFuture 还是 Spring @Async?为什么?

参考答案

对比维度 CompletableFuture Spring @Async
编排能力 强(链式组合、并行、合并) 弱(只能简单异步执行)
异常处理 灵活(exceptionally、handle) 需要 AsyncUncaughtExceptionHandler
线程池控制 精确(每个任务指定线程池) 全局配置,粒度粗
Spring 集成 无缝(配合 @Async 也可) 原生支持
返回值 CompletableFuture Future / CompletableFuture
学习成本 中等

我的选择

  • 简单的异步操作(发邮件、写日志):用 @Async,简单省事
  • 复杂的编排场景(多服务聚合、超时控制、容错降级):用 CompletableFuture,灵活强大
  • 两者可以配合使用:@Async 方法返回 CompletableFuture,外部调用者可以继续编排

场景题5:异步编排中的线程池隔离设计

面试官:你们项目里有几个线程池?怎么隔离的?

参考答案

我们的线程池隔离策略如下:

java 复制代码
@Configuration
public class ThreadPoolConfig {

    // CPU密集型线程池(计算类任务)
    @Bean("cpuPool")
    public ExecutorService cpuPool() {
        int cores = Runtime.getRuntime().availableProcessors();
        return new ThreadPoolExecutor(
                cores, cores,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(100),
                new NamedThreadFactory("cpu-pool")
        );
    }

    // IO密集型线程池(RPC调用、HTTP请求)
    @Bean("ioPool")
    public ExecutorService ioPool() {
        return new ThreadPoolExecutor(
                20, 100,
                60L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(2000),
                new NamedThreadFactory("io-pool"),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }

    // 快速任务线程池(本地缓存、简单计算)
    @Bean("fastPool")
    public ExecutorService fastPool() {
        return new ThreadPoolExecutor(
                10, 20,
                30L, TimeUnit.SECONDS,
                new SynchronousQueue<>(),
                new NamedThreadFactory("fast-pool")
        );
    }
}

隔离原则

线程池 用途 特点
cpuPool 数据计算、加密解密 线程数=CPU核心数,避免上下文切换
ioPool RPC调用、数据库查询、HTTP请求 线程数较多,队列较大,适应IO等待
fastPool 本地缓存读写、简单转换 小队列 + SynchronousQueue,快速失败

这样做的好处是:IO 任务不会阻塞 CPU 任务,快速任务不会被慢任务拖累,出了问题也容易定位是哪个池子出了状况。


九、互动话题

你在项目中用 CompletableFuture 遇到过什么坑?是线程池配错、异常被吞、还是 ThreadLocal 丢失?当时是怎么解决的?欢迎在评论区聊聊,我会在评论区和大家一起讨论最佳实践。


参考资料