CompletableFuture 如何简化 Java 中的异步编程?
Source: Dev.to
请提供您希望翻译的正文内容,我将为您翻译成简体中文并保留原有的格式、Markdown 语法以及技术术语。
概述
想象一下,你使用一个应用点餐。
你不会盯着屏幕发呆——而是刷社交媒体、回复消息,或在食物烹饪期间观看视频。
这正是异步编程应该的工作方式。
在传统的 Java 中,编写异步代码通常意味着:
- 手动管理线程
- 处理回调
- 编写复杂、难以阅读的逻辑
于是出现了 CompletableFuture。
CompletableFuture 让 Java 中的异步编程变得更简洁、可读且可组合——而且不需要你成为并发专家。
在本博客中,你将学习**CompletableFuture 如何简化 Java 中的异步编程**,我们将一步步解释,并提供一个完整可运行的 Spring Boot 端到端示例,包括cURL 请求和真实响应。
核心概念
什么是 CompletableFuture?
CompletableFuture 是一个 Java 类,代表 将在未来某个时间完成的计算。
与阻塞等待不同,你可以:
- 异步启动任务
- 链接更多任务
- 干净地处理结果和错误
为什么 CompletableFuture 比传统线程更好
在使用 CompletableFuture 之前:
new Thread(() -> doWork()).start();
问题
- ❌ 难以管理
- ❌ 错误处理不便
- ❌ 无法链式调用
使用 CompletableFuture:
CompletableFuture.supplyAsync(() -> doWork())
.thenApply(result -> transform(result))
.thenAccept(finalResult -> use(finalResult));
代码示例
CompletableFuture.supplyAsync(() -> doWork())
.thenApply(result -> transform(result))
.thenAccept(finalResult -> use(finalResult));
大局观(一句话概括)
这段代码异步执行任务,转换其结果,然后消费最终输出——整个过程不会阻塞主线程。
现实类比(点餐)
1️⃣ 下单 → 厨房开始烹饪
2️⃣ 食物准备好 → 加调味料
3️⃣ 上菜 → 你开始吃
你不需要站在厨房等——所有步骤按顺序、异步进行。
步骤分解
CompletableFuture.supplyAsync(() -> doWork())
CompletableFuture.supplyAsync(() -> doWork())
- 它的作用: 在后台线程中执行
doWork(),并立即返回一个CompletableFuture。 - 主线程: 不会被阻塞。
- 类比: “开始做这件事,等完成后再通知我”。
示例
String doWork() {
return "raw data";
}
输出(未来结果): "raw data"
.thenApply(result -> transform(result))
.thenApply(result -> transform(result))
-
它的作用:
- 等待
doWork()完成。 - 获取它的结果(
result)。 - 将其转换 为其他内容。
- 返回一个 新的
CompletableFuture。
- 等待
-
关键规则:
thenApply用于 转换数据。
示例
String transform(String input) {
return input.toUpperCase();
}
输入: "raw data"
输出: "RAW DATA"
.thenAccept(finalResult -> use(finalResult))
.thenAccept(finalResult -> use(finalResult));
- 它的作用: 接收最终的转换结果,消费它,并且不返回任何值。
- 关键规则:
thenAccept用于 副作用,而不是转换。
示例
void use(String data) {
System.out.println(data);
}
输出
RAW DATA
完整流程(可视化)
doWork()
↓
thenApply (transform)
↓
thenAccept (consume)
或作为管道:
供应商 → 转换器 → 消费者
具体示例(完整工作)
CompletableFuture.supplyAsync(() -> "hello world")
.thenApply(result -> result.toUpperCase())
.thenAccept(finalResult -> System.out.println(finalResult));
输出
HELLO WORLD
为什么这很强大
没有 CompletableFuture | 使用 CompletableFuture |
|---|---|
| 阻塞调用 | 非阻塞 |
| 手动线程处理 | 自动 |
| 回调地狱 | 干净的链式调用 |
| 困难的错误处理 | 结构化 |
常见错误
- ❌ 将
thenApply用于副作用 - ❌ 不必要地调用
.get()或.join() - ❌ 在异步阶段中进行阻塞
- ❌ 忽略异常
经验法则(记住它)
| 方法 | 使用场景 |
|---|---|
supplyAsync | 当你想启动异步工作 |
thenApply | 当你想 转换 数据 |
thenAccept | 当你想 使用 数据(副作用) |
thenRun | 当你不需要数据时 |
Final Summary
这段代码:
- 启动异步工作
- 转换结果
- 消费最终输出
- 永不阻塞主线程
它是 干净、可读且可扩展 —— 正是现代 Java 异步代码的应有姿态。
- ✔ 可读
- ✔ 可组合
- ✔ 非阻塞
常见使用场景
- ✅ 调用外部 API
- ✅ 并行服务调用
- ✅ 后台处理
- ✅ 提升 API 响应时间
- ✅ 非阻塞操作
工作流
代码示例(端到端)
示例 1:基本 CompletableFuture(纯 Java)
使用场景 – 异步获取数据并进行处理。
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> {
// Simulate long‑running task
sleep(1000);
return "Async result";
});
future.thenApply(result -> result.toUpperCase())
.thenAccept(finalResult ->
System.out.println("Received: " + finalResult));
// Prevent JVM from exiting early
sleep(2000);
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ignored) {
}
}
}
我们将讨论的内容
- 程序整体功能
- 每个部分的详细说明
- 为何需要
sleep(2000) - 逐步运行时行为
大局观(单行概述)
程序以异步方式运行任务,转换其结果,打印出来,并保持 JVM 存活足够长的时间,以便异步工作完成。
Source: …
完整代码(参考)
(与上面的代码块相同 – 这里保留以便快速参考。)
CompletableFuture.supplyAsync(...)
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "Async result";
});
作用说明
- 在后台线程(ForkJoinPool 公共池)中启动一个任务。
- 任务休眠 1 秒(模拟慢速工作),并返回
"Async result"。 - 立即返回一个
CompletableFuture;main线程不会等待。
重要提示:
main线程会继续执行,而异步工作在后台运行。
thenApply(...) – 转换结果
future.thenApply(result -> result.toUpperCase())
作用说明
- 等待异步任务完成。
- 获取结果(
"Async result"),将其转为大写,并传递给下一个阶段。
| 输入 | 输出 |
|---|---|
"Async result" | "ASYNC RESULT" |
规则: 当需要转换数据时使用
thenApply。
thenAccept(...) – 消费结果
.thenAccept(finalResult ->
System.out.println("Received: " + finalResult));
作用说明
- 接收已转换的值。
- 打印它。
- 不返回任何内容(
void)。
打印输出
Received: ASYNC RESULT
规则: 使用
thenAccept进行副作用(日志记录、打印、保存等)。
为什么在 main 中需要 sleep(2000)
sleep(2000);
- 这行代码保持 JVM 存活足够长的时间,以便异步工作完成。
- 如果没有它,
main()会立即结束,JVM 退出,异步任务可能根本不会完成。
在实际应用中(Spring Boot、服务器等),JVM 会自动保持存活,因此不需要这种人为的休眠。
Source: …
执行时间线(逐步)
| 时间 (毫秒) | 操作 |
|---|---|
| 0 | main() 开始;supplyAsync() 将任务提交给后台线程。 |
| 1 | main() 注册 thenApply 和 thenAccept。 |
| 1000 | 后台线程完成 sleep(1000) 并返回 "Async result"。 |
| ~1000 | thenApply 运行 → "ASYNC RESULT";thenAccept 运行 → 打印结果。 |
| 2000 | main() 完成 sleep(2000),JVM 安全退出。 |
可视化流程
Main Thread
|
|-- submit async task -------------------->
|
|-- register thenApply
|-- register thenAccept
|
|-- sleep(2000) ← keeps JVM alive
|
Background Thread
|
|-- sleep(1000)
|-- return "Async result"
|-- transform to "ASYNC RESULT"
|-- print result
常见初学者错误
- ❌ 在独立程序中删除
sleep(2000)。 - ❌ 误以为异步代码块会自动执行。
- ❌ 使用
thenApply来打印(应使用thenAccept)。 - ❌ 不必要地调用
.get(),这会抵消非阻塞代码的意义。
关键要点(记住这些)
| 操作 | 目的 |
|---|---|
supplyAsync | 在单独的线程中启动异步工作。 |
thenApply | 转换前一个阶段的结果。 |
thenAccept | 消费结果(副作用)。 |
main 中的 sleep | 保持 JVM 运行,以演示程序。 |
欢迎通过更改 sleep 时长、转换逻辑,或链式添加更多阶段来进行实验!
Source: …
示例 2:Spring Boot + CompletableFuture(100 % 端到端)
用例
暴露一个 REST API,异步执行工作并返回结果。
步骤 1:服务层(异步逻辑)
package com.example.demo.service;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class AsyncService {
public CompletableFuture<String> processAsync() {
return CompletableFuture.supplyAsync(() -> {
simulateDelay();
return "Async processing completed";
});
}
private void simulateDelay() {
try {
Thread.sleep(1500);
} catch (InterruptedException ignored) {
}
}
}
步骤 2:REST 控制器
package com.example.demo.controller;
import com.example.demo.service.AsyncService;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/async")
public class AsyncController {
private final AsyncService asyncService;
public AsyncController(AsyncService asyncService) {
this.asyncService = asyncService;
}
/**
* Example:
* GET /async/process
*/
@GetMapping("/process")
public CompletableFuture<String> processAsync() {
return asyncService.processAsync();
}
}
📌 Spring Boot 自动
- 检测
CompletableFuture - 释放请求线程
- 在计算完成后写入响应
步骤 3:使用 cURL 测试
请求
curl --location 'http://localhost:8080/async/process'
响应(约 1.5 秒后)
Async processing completed
优势
- ✔ 非阻塞请求
- ✔ 线程高效
- ✔ 干净的异步 API
这到底发生了什么?
- HTTP 请求到达控制器。
- 控制器返回一个
CompletableFuture。 - Servlet 线程被释放。
- 异步任务在后台运行。
- 当 future 完成时写入响应。
这 正是高性能 API 的构建方式。
最佳实践
- 避免在
CompletableFuture中进行阻塞调用 – 不要将异步代码与大量阻塞逻辑混合。 - 使用链式调用而非嵌套回调 – 推荐使用
thenApply、thenCompose、thenAccept。