【多线程编程】CompletableFuture 使用指南(基础篇):从原理到 API

文章目录

    • [一、为什么需要 CompletableFuture(Why CompletableFuture):解决异步编程的核心痛点](#一、为什么需要 CompletableFuture(Why CompletableFuture):解决异步编程的核心痛点)
      • [1.1 传统 Future 的困境:串行慢、并行复杂](#1.1 传统 Future 的困境:串行慢、并行复杂)
      • [1.2 CompletableFuture 的解决方案:简单、高效、可靠](#1.2 CompletableFuture 的解决方案:简单、高效、可靠)
    • [二、CompletableFuture 的实现原理(Implementation Principles):理解其设计思想](#二、CompletableFuture 的实现原理(Implementation Principles):理解其设计思想)
      • [2.1 核心数据结构:状态机 + 回调链表(实现无锁异步编排的基础)](#2.1 核心数据结构:状态机 + 回调链表(实现无锁异步编排的基础))
      • [2.2 CAS 无锁机制:保证线程安全的高性能方案(避免锁竞争,提升并发性能)](#2.2 CAS 无锁机制:保证线程安全的高性能方案(避免锁竞争,提升并发性能))
      • [2.3 回调链机制:实现任务组合的核心(延迟执行,链式传播)](#2.3 回调链机制:实现任务组合的核心(延迟执行,链式传播))
      • [2.4 线程池复用:默认使用公共线程池(合理配置线程池,避免资源浪费)](#2.4 线程池复用:默认使用公共线程池(合理配置线程池,避免资源浪费))
    • [三、核心 API 详解(Core APIs):从基础到高级](#三、核心 API 详解(Core APIs):从基础到高级)
      • [3.1 创建异步任务:supplyAsync vs runAsync](#3.1 创建异步任务:supplyAsync vs runAsync)
      • [3.2 转换操作:thenApply vs thenCompose](#3.2 转换操作:thenApply vs thenCompose)
      • [3.3 组合操作:thenCombine vs allOf](#3.3 组合操作:thenCombine vs allOf)
      • [3.4 异常处理:exceptionally vs handle](#3.4 异常处理:exceptionally vs handle)

想象一下这样的场景:你需要查询用户信息、订单列表和积分数据,传统方式是一个接一个地等待,总耗时 650 毫秒。而使用 CompletableFuture,这三个查询可以同时进行,总耗时只需要 300 毫秒------这就是异步编程带来的性能飞跃。

就像餐厅里,传统方式是一个服务员按顺序服务每桌客人,而 CompletableFuture 让多个服务员同时工作,最后统一汇总结果。这种"并行执行、统一汇总"的设计,让 Java 并发编程从"复杂的手动管理"变成了"简单的链式组合"。

核心要点

  1. 非阻塞执行:任务在后台线程执行,主线程不被阻塞,可以继续处理其他逻辑
  2. 灵活组合:可以轻松组合多个异步任务,实现复杂的业务逻辑链
  3. 异常传播:完善的异常处理机制,让异步编程的错误处理变得简单可靠
  4. 性能提升:通过并行执行,将串行耗时变为并行耗时,性能提升可达数倍

一、为什么需要 CompletableFuture(Why CompletableFuture):解决异步编程的核心痛点

核心结论:CompletableFuture 解决了传统 Future 的阻塞等待、组合困难、异常处理复杂三大痛点,让异步编程从"技术实现"变成了"业务表达"。

1.1 传统 Future 的困境:串行慢、并行复杂

很多人都有这样的困扰:当我们需要并行执行多个任务时,传统的方式要么是串行等待(慢),要么是手动管理线程(复杂)。

以查询用户信息为例,传统方式可能是这样:

java 复制代码
// 串行方式:总耗时 = 查询用户 + 查询订单 + 查询积分
UserInfo user = userService.getUserInfo(userId);        // 耗时 200ms
List<Order> orders = orderService.getOrders(userId);    // 耗时 300ms
PointsInfo points = pointsService.getPoints(userId);    // 耗时 150ms
// 总耗时:650ms

如果用 Future,虽然可以并行,但代码复杂:

java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<UserInfo> userFuture = executor.submit(() -> userService.getUserInfo(userId));
Future<List<Order>> ordersFuture = executor.submit(() -> orderService.getOrders(userId));
Future<PointsInfo> pointsFuture = executor.submit(() -> pointsService.getPoints(userId));

// 需要手动等待和获取结果
UserInfo user = userFuture.get();  // 阻塞等待
List<Order> orders = ordersFuture.get();  // 阻塞等待
PointsInfo points = pointsFuture.get();  // 阻塞等待
// 总耗时:300ms(最慢的那个),但代码复杂,异常处理困难

1.2 CompletableFuture 的解决方案:简单、高效、可靠

CompletableFuture 让这一切变得简单:

java 复制代码
CompletableFuture<UserInfo> userFuture = CompletableFuture.supplyAsync(() -> 
    userService.getUserInfo(userId)
);
CompletableFuture<List<Order>> ordersFuture = CompletableFuture.supplyAsync(() -> 
    orderService.getOrders(userId)
);
CompletableFuture<PointsInfo> pointsFuture = CompletableFuture.supplyAsync(() -> 
    pointsService.getPoints(userId)
);

// 等待所有完成并组合结果
CompletableFuture.allOf(userFuture, ordersFuture, pointsFuture).join();
// 总耗时:300ms,代码简洁,异常处理完善

allOf(...).join() 方法详解

CompletableFuture.allOf(...).join() 是等待多个并行任务完成的常用模式,由两部分组成:

任务执行时机

任务在 supplyAsync() 调用时立即提交到线程池并开始执行 ,而不是在 allOf()join() 时才开始:

java 复制代码
// 第1行:立即提交到线程池,任务开始执行(异步)
CompletableFuture<UserInfo> userFuture = CompletableFuture.supplyAsync(() -> 
    userService.getUserInfo(userId)  // ← 此时任务已经在后台线程执行
);

// 第2行:立即提交到线程池,任务开始执行(异步)
CompletableFuture<List<Order>> ordersFuture = CompletableFuture.supplyAsync(() -> 
    orderService.getOrders(userId)  // ← 此时任务已经在后台线程执行
);

// 第3行:立即提交到线程池,任务开始执行(异步)
CompletableFuture<PointsInfo> pointsFuture = CompletableFuture.supplyAsync(() -> 
    pointsService.getPoints(userId)  // ← 此时任务已经在后台线程执行
);

// 第4行:只是等待所有任务完成,不触发执行(任务已经在执行中)
CompletableFuture.allOf(userFuture, ordersFuture, pointsFuture).join();
// ↑ 此时三个任务可能已经完成,也可能还在执行中

关键理解

  • supplyAsync():立即提交任务到线程池,任务开始异步执行
  • allOf():只等待完成,不触发执行(任务已经在执行)
  • join():阻塞等待,不触发执行(任务已经在执行)
  1. allOf(...)

    • 静态方法,接收多个 CompletableFuture 作为参数
    • 返回一个新的 CompletableFuture<Void>,当所有传入的任务都完成时(无论成功还是失败),这个新的 Future 才会完成
    • 不关心各个任务的具体返回值,只关心是否全部完成
    • 不触发任务执行 :任务在 supplyAsync() 时已经执行
  2. .join()

    • 阻塞当前线程,直到所有任务完成
    • 如果所有任务成功完成,返回 null(因为 allOf 返回 CompletableFuture<Void>
    • 如果任何一个任务失败,会抛出 CompletionException,包装原始异常
    • 不触发任务执行:只是等待,任务已经在执行中

执行流程示例

复制代码
时间线:
0ms    → 启动3个异步任务(并行执行)
        ├─ userFuture    (100ms)
        ├─ ordersFuture  (200ms)  
        └─ pointsFuture  (300ms)

100ms  → userFuture 完成 ✓
200ms  → ordersFuture 完成 ✓
300ms  → pointsFuture 完成 ✓
        → allOf().join() 返回(所有任务完成)

关键特性

  • 并行执行:三个任务同时启动,并行执行,而不是串行等待
  • 阻塞等待join() 会阻塞当前线程,直到最慢的任务完成(300ms)
  • 异常处理 :如果任何一个任务失败,join() 会抛出异常,需要 try-catch 处理

获取结果

java 复制代码
// allOf().join() 只等待完成,不返回结果
// 需要单独调用各 future 的 join() 获取结果
allOf(userFuture, ordersFuture, pointsFuture).join();

UserInfo user = userFuture.join();      // 立即返回(已完成)
List<Order> orders = ordersFuture.join(); // 立即返回(已完成)
PointsInfo points = pointsFuture.join();  // 立即返回(已完成)

二、CompletableFuture 的实现原理(Implementation Principles):理解其设计思想

核心结论:CompletableFuture 通过 CAS(Compare-And-Swap)操作和栈式回调链表实现无锁的异步任务编排,这是其高性能和灵活性的基础。理解这个原理,就能明白为什么 CompletableFuture 既能保证线程安全,又能实现高效的任务组合。

2.1 核心数据结构:状态机 + 回调链表(实现无锁异步编排的基础)

CompletableFuture 的核心是一个状态机,通过 volatile 变量和 CAS 操作保证线程安全:

java 复制代码
// 核心字段
volatile Object result;           // 存储结果或异常
volatile Completion stack;        // 回调链表的栈顶

设计思路

  • result 字段:存储任务结果,如果任务未完成则为 null,完成时存储结果或异常
  • stack 字段:存储所有等待此任务完成的后续操作(回调),形成一个链表栈

2.2 CAS 无锁机制:保证线程安全的高性能方案(避免锁竞争,提升并发性能)

核心结论:CompletableFuture 使用 CAS 操作来更新状态,避免了传统锁的开销,这是其高性能的关键。

java 复制代码
// 伪代码:完成任务的逻辑
boolean complete(T value) {
    // 使用 CAS 原子性地设置结果
    if (CAS(result, null, value)) {
        // 成功设置结果,触发回调链
        postComplete();
        return true;
    }
    return false;  // 已经被其他线程完成
}

设计优势

  • 无锁设计:避免了锁竞争,提高了并发性能
  • 原子操作:CAS 保证状态更新的原子性,线程安全
  • 高性能:无锁设计让 CompletableFuture 在高并发场景下表现优异

2.3 回调链机制:实现任务组合的核心(延迟执行,链式传播)

核心结论:当任务完成时,会触发回调链的执行,这是实现任务组合的核心机制。

当任务完成时,会触发回调链的执行:

java 复制代码
// 伪代码:触发回调链
void postComplete() {
    Completion h = stack;  // 获取回调链栈顶
    while (h != null) {
        Completion next = h.next;
        h.tryFire();  // 执行回调
        h = next;
    }
}

设计优势

  • 延迟执行:回调不会立即执行,而是等到任务完成时才执行
  • 链式传播:一个任务的完成会触发后续任务的执行,形成链式反应
  • 线程安全:通过 CAS 操作保证回调链的线程安全

2.4 线程池复用:默认使用公共线程池(合理配置线程池,避免资源浪费)

核心结论 :CompletableFuture 默认使用 ForkJoinPool.commonPool(),这是一个共享的线程池,合理配置线程池可以避免资源浪费。

CompletableFuture 默认使用 ForkJoinPool.commonPool(),这是一个共享的线程池:

java 复制代码
// 默认使用公共线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
    return asyncSupplyStage(ASYNC_POOL, supplier);
}

// 也可以指定自定义线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) {
    return asyncSupplyStage(screenExecutor(executor), supplier);
}

最佳实践

  • CPU 密集型任务 :使用 ForkJoinPool,线程数 = CPU 核心数
  • IO 密集型任务:使用自定义线程池,线程数可以更大(如 50-100)
  • 避免线程泄漏:长时间运行的任务建议使用自定义线程池,便于管理

三、核心 API 详解(Core APIs):从基础到高级

核心结论:CompletableFuture 的 API 分为创建、转换、组合、等待四大类,理解每类的使用场景和原理,才能灵活运用。掌握了这四类 API,就能解决 90% 的并发编程问题。

3.1 创建异步任务:supplyAsync vs runAsync

核心结论 :根据任务是否有返回值,选择 supplyAsyncrunAsync

java 复制代码
// 有返回值的任务
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    return "结果";
});

// 无返回值的任务
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
    System.out.println("执行完成");
});

// 已完成的任务(用于测试或组合)
CompletableFuture<String> future3 = CompletableFuture.completedFuture("已完成");

适用场景

  • supplyAsync:需要返回结果的异步操作(如查询数据库、调用 API)
  • runAsync:只需要执行操作,不需要结果(如发送日志、清理缓存)
  • completedFuture:测试场景,或者需要统一接口的已完成任务

3.2 转换操作:thenApply vs thenCompose

核心结论thenApply 是同步转换,在当前线程执行;thenCompose 是异步转换,返回新的 CompletableFuture 在后台线程执行。

java 复制代码
// thenApply:同步转换,在当前线程执行
CompletableFuture<String> future1 = CompletableFuture
    .supplyAsync(() -> "Hello")
    .thenApply(s -> s + " World");  // 同步转换

// thenCompose:异步转换,返回新的 CompletableFuture
CompletableFuture<String> future2 = CompletableFuture
    .supplyAsync(() -> "Hello")
    .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " World"));  // 异步转换

执行时机和线程

thenApply

  • 执行时机:在前一个任务完成后,立即在当前线程(完成任务的线程)执行
  • 返回值 :直接返回转换后的结果(CompletableFuture<U>
  • 线程模型:同步执行,不创建新线程

thenCompose

  • 执行时机:在前一个任务完成后,提交新的异步任务到线程池执行
  • 返回值 :返回一个新的 CompletableFuture<U>,这个 Future 代表新任务的执行
  • 线程模型:异步执行,创建新线程

实际应用场景对比

场景1:轻量级转换(使用 thenApply

java 复制代码
// 字符串拼接、数据格式化等轻量级操作
CompletableFuture<String> userInfo = CompletableFuture
    .supplyAsync(() -> userService.getUser(userId))
    .thenApply(user -> user.getName() + " (" + user.getEmail() + ")");
// 总耗时:查询用户时间(转换几乎不耗时)

场景2:重量级操作(使用 thenCompose

java 复制代码
// 数据库查询、网络请求等重量级操作
CompletableFuture<String> userProfile = CompletableFuture
    .supplyAsync(() -> userService.getUser(userId))  // 100ms
    .thenCompose(user -> 
        CompletableFuture.supplyAsync(() -> 
            orderService.getOrders(user.getId())  // 200ms,异步执行
        )
    );
// 总耗时:100ms + 200ms = 300ms(如果串行执行)
// 但如果两个任务可以并行,应该用 allOf 而不是 thenCompose

选择原则

  • 轻量级操作 (如字符串拼接、简单计算、数据格式化):使用 thenApply
    • 优点:不创建新线程,开销小
    • 缺点:会阻塞完成任务的线程
  • 重量级操作 (如数据库查询、网络请求、文件IO):使用 thenCompose
    • 优点:不阻塞线程,可以充分利用线程池
    • 缺点:创建新线程,有额外开销

为什么重量级操作不能用 thenApply

这是一个常见误区。技术上 thenApply 可以用于重量级操作,但会有严重问题:

java 复制代码
// ❌ 问题示例:使用 thenApply 执行重量级操作
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> "Hello")  // 任务1:快速完成
    .thenApply(s -> {
        // 问题:这个数据库查询会在哪个线程执行?
        return databaseService.query(s);  // 重量级操作,耗时 200ms
    });

thenApply 的执行线程问题

  1. 如果前一个任务已完成

    java 复制代码
    CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Hello");
    f1.join();  // 等待完成
    
    // 此时 thenApply 会在调用它的线程中执行(可能是主线程)
    CompletableFuture<String> f2 = f1.thenApply(s -> {
        // 在主线程中执行!会阻塞主线程
        return databaseService.query(s);  // 阻塞主线程 200ms
    });
  2. 如果前一个任务未完成

    java 复制代码
    CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
        Thread.sleep(100);
        return "Hello";
    });
    
    // thenApply 会在任务1完成的线程中执行
    CompletableFuture<String> f2 = f1.thenApply(s -> {
        // 在任务1的线程中执行,会阻塞这个线程
        return databaseService.query(s);  // 阻塞线程池线程 200ms
    });

核心问题

  • 阻塞线程thenApply 会阻塞执行它的线程(无论是主线程还是线程池线程)
  • 线程资源浪费:线程被阻塞,无法处理其他任务
  • 性能下降:如果线程池线程被阻塞,其他任务可能无法及时执行

正确的做法(使用 thenCompose

java 复制代码
// ✅ 正确:使用 thenCompose 异步执行重量级操作
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> "Hello")
    .thenCompose(s -> {
        // 提交新的异步任务,不阻塞当前线程
        return CompletableFuture.supplyAsync(() -> 
            databaseService.query(s)  // 在新线程中执行,不阻塞
        );
    });

性能对比

java 复制代码
// 场景:100个并发请求,每个需要查询数据库

// ❌ 使用 thenApply(阻塞线程)
// 如果线程池有10个线程,每个线程被阻塞200ms
// 100个请求需要:100 / 10 * 200ms = 2000ms

// ✅ 使用 thenCompose(不阻塞线程)
// 线程池线程可以快速处理请求,只负责提交任务
// 100个请求可以并行执行,总耗时约 200ms(最慢的那个)

总结

  • thenApply 可以用于重量级操作,但会阻塞线程,导致性能问题
  • thenCompose 是更好的选择,因为它不阻塞线程,可以充分利用线程池
  • 选择原则 :如果操作耗时超过几毫秒,就应该用 thenCompose 异步执行

注意事项

  • thenApply 返回 CompletableFuture<U>,但转换是同步的
  • thenCompose 返回 CompletableFuture<U>,转换是异步的
  • 如果多个任务可以并行执行,应该用 allOf 而不是 thenComposethenCompose 是串行的)

3.3 组合操作:thenCombine vs allOf

核心结论thenCombine 组合两个任务的结果,allOf 等待所有任务完成。

java 复制代码
// thenCombine:组合两个任务的结果
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<String> combined = future1.thenCombine(future2, (s1, s2) -> s1 + " " + s2);

// allOf:等待所有任务完成
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Task1");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "Task2");
CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> "Task3");
CompletableFuture.allOf(f1, f2, f3).thenRun(() -> {
    // 所有任务完成后的处理
});

使用场景

  • thenCombine:需要两个任务的结果进行组合(如合并两个查询结果)
  • allOf:需要等待多个任务全部完成(如批量处理)

3.4 异常处理:exceptionally vs handle

核心结论exceptionally 只处理异常,handle 同时处理成功和异常。

java 复制代码
// exceptionally:只处理异常
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> {
        if (Math.random() > 0.5) {
            throw new RuntimeException("随机异常");
        }
        return "成功";
    })
    .exceptionally(ex -> {
        log.error("处理异常", ex);
        return "默认值";
    });

// handle:同时处理成功和异常
CompletableFuture<String> future2 = CompletableFuture
    .supplyAsync(() -> "结果")
    .handle((result, ex) -> {
        if (ex != null) {
            return "异常处理";
        }
        return result;
    });

选择原则

  • 只需要异常处理 :使用 exceptionally
  • 需要统一处理成功和异常 :使用 handle

相关推荐
这周也會开心3 天前
多线程与并发-知识总结1
java·多线程·并发
C雨后彩虹5 天前
synchronized高频考点模拟面试过程
java·面试·多线程·并发·lock
曲幽11 天前
FastAPI + TinyDB并发陷阱与实战:告别数据错乱的解决方案
python·json·fastapi·web·并发·queue·lock·文件锁·tinydb
七夜zippoe14 天前
Python并发与并行编程深度剖析:从GIL原理到高并发实战
服务器·网络·python·并发·gil
C++chaofan15 天前
JUC 并发编程从入门到精通(超详细笔记 + 实战案例)
java·jvm·spring boot·redis·后端·并发·juc
xj75730653317 天前
并发编程基础介绍
并发·并行
编程武士19 天前
纤程概念浅析
并发·概念·fiber
Neolnfra23 天前
当“同时发生”成为攻击武器
web安全·网络安全·并发·高并发产生的漏洞
apocelipes1 个月前
从源码角度解析C++20新特性如何简化线程超时取消
c++·性能优化·golang·并发·c++20·linux编程