1. 概述

在现代软件开发中,高性能和高可用性是核心要素。实现这一目标的关键方式之一就是采用非阻塞和异步编程。在 Java 中,CompletableFuture 类提供了一种编写非阻塞代码的途径。但它真的是非阻塞的吗?

本文将深入探讨 CompletableFuture 在哪些情况下是非阻塞的,哪些情况下会变成阻塞操作。

2. CompletableFuture 简介

首先快速了解下 CompletableFuture 类。它是 Java 8 引入的强大工具,属于并发 API 的一部分。

该类实现了 Future 接口,同时也是 CompletionStage 接口的主要实现。因此它提供了近 50 种不同的方法来创建和执行异步计算。

为什么需要 CompletableFuture?传统的 Future 接口只能通过调用 get() 方法获取结果,但这是一种阻塞操作——它会冻结当前线程直到任务完成。如果需要对结果执行后续操作,就会陷入阻塞困境。

CompletableFuture 通过 CompletionStage 提供了链式调用能力,允许将多个计算任务串联起来并发执行。这种机制让我们能创建任务链,当前任务完成后自动触发下一个任务。

更重要的是,我们可以在不阻塞当前线程的情况下,指定获取结果后的处理逻辑。CompletableFuture 既代表依赖流程中的阶段(一个阶段的完成触发另一个阶段),也代表计算结果本身。

3. 阻塞 vs 非阻塞

接下来理解阻塞与非阻塞处理的区别:

在阻塞操作中,调用线程必须等待另一个线程的操作完成后才能继续执行:

阻塞处理示意图

如图所示,任务按顺序执行。线程 1 被线程 2 阻塞——线程 1 必须等待线程 2 完成任务才能继续。这本质上是同步操作。

⚠️ 阻塞操作会导致性能问题,尤其在高可用和高可扩展性应用中。

而非阻塞操作允许线程同时执行多个计算,无需等待任务完成:

非阻塞处理示意图

这里线程 2 不会阻塞线程 1 的执行,两个线程并发运行各自任务。除了提升性能,我们还能在非阻塞操作完成后决定如何处理结果。

4. CompletableFuture 与非阻塞操作

CompletableFuture 的核心优势在于能链式调用任务且不阻塞当前线程。因此可以说 CompletableFuture 本质是非阻塞的

它提供了多个支持非阻塞操作的关键方法:

supplyAsync():异步执行任务并返回代表结果的 CompletableFuture
thenApply():对前序任务结果应用函数,返回转换后的 CompletableFuture
thenCompose():执行返回 CompletableFuture 的任务,返回嵌套任务结果的 CompletableFuture
allOf():并行执行多个任务,返回代表所有任务完成的 CompletableFuture

举个简单例子,假设有两个任务需要非阻塞执行:

CompletableFuture.supplyAsync(() -> "Baeldung")
  .thenApply(String::length)
  .thenAccept(s -> logger.info(String.valueOf(s)));

任务完成后会输出数字 8。计算在后台运行并返回 Future,每个依赖操作都是一个阶段。前序阶段完成后会自动触发后续阶段的计算。

5. 何时 CompletableFuture 会阻塞?

尽管 CompletableFuture 用于非阻塞操作,但在某些场景下仍会阻塞当前线程。

异步通信通常通过回调机制获取结果,但 CompletableFuture 完成时不会主动通知。如果需要在调用线程获取结果,可以使用 get() 方法。

⚠️ 注意:get() 方法通过阻塞方式返回结果。它会等待计算完成再返回结果,因此会阻塞当前线程直到 Future 完成:

CompletableFuture<String> completableFuture = CompletableFuture
  .supplyAsync(() -> "Baeldung")
  .thenApply(String::toUpperCase);

assertEquals("BAELDUNG", completableFuture.get());

类似地,调用 join() 方法也会阻塞当前线程:

CompletableFuture<String> completableFuture = CompletableFuture
  .supplyAsync(() -> "Blocking")
  .thenApply(s -> s + " Operation")
  .thenApply(String::toLowerCase);

assertEquals("blocking operation", completableFuture.join());

这两个方法的主要区别是:join() 在 Future 异常完成时不会抛出受检异常。

虽然可以通过 isDone() 检查 Future 是否完成,但当必须在调用线程获取结果时,常见做法是:创建 CompletableFuture → 在当前线程执行其他工作 → 调用 get()/join()。通过延长等待时间,Future 更可能在获取结果前完成计算,但仍无法完全避免阻塞风险

6. 结论

本文分析了 CompletableFuture 的非阻塞与阻塞场景:

✅ 大多数情况下 CompletableFuture 是非阻塞的
❌ 但调用 get()join() 获取结果时会阻塞当前线程

完整源代码可在 GitHub 获取。


原始标题:Is CompletableFuture Non-blocking?