CompletableFuture 如何简化 Java 中的异步编程?

发布: (2025年12月26日 GMT+8 20:02)
11 min read
原文: Dev.to

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))
  • 它的作用:

    1. 等待 doWork() 完成。
    2. 获取它的结果(result)。
    3. 将其转换 为其他内容。
    4. 返回一个 新的 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"
  • 立即返回一个 CompletableFuturemain 线程不会等待。

重要提示: 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:

执行时间线(逐步)

时间 (毫秒)操作
0main() 开始;supplyAsync() 将任务提交给后台线程。
1main() 注册 thenApplythenAccept
1000后台线程完成 sleep(1000) 并返回 "Async result"
~1000thenApply 运行 → "ASYNC RESULT"thenAccept 运行 → 打印结果。
2000main() 完成 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

这到底发生了什么?

  1. HTTP 请求到达控制器。
  2. 控制器返回一个 CompletableFuture
  3. Servlet 线程被释放。
  4. 异步任务在后台运行。
  5. 当 future 完成时写入响应。

正是高性能 API 的构建方式

最佳实践

  • 避免在 CompletableFuture 中进行阻塞调用 – 不要将异步代码与大量阻塞逻辑混合。
  • 使用链式调用而非嵌套回调 – 推荐使用 thenApplythenComposethenAccept
Back to Blog

相关文章

阅读更多 »