CodeGym /课程 /JAVA 25 SELF /CompletableFuture 入门

CompletableFuture 入门

JAVA 25 SELF
第 55 级 , 课程 0
可用

1. 同步代码的问题

设想一下:你有一个程序,需要从网络加载数据或读取一个大文件。你会写出类似这样的代码:

String data = readFromFile("bigfile.txt");
System.out.println("数据: " + data);

一切看起来都不错,但如果文件很大或者网络很慢,程序就会在读取这行上直接 卡住。用户看到一个“卡住”的界面,服务器无法处理其他请求,而程序员……只能叹气。

这种情况就叫做 阻塞:线程(例如你应用的主线程)被迫等待操作完成。而如果这样的操作很多——那就会出现卡顿和低性能问题。

这有点像你去咖啡店点了单,然后……必须一直站在吧台前等到咖啡做出来。其他客人也在你后面排队,等咖啡师先把你的搞定。效率很低,对吧?

异步如何拯救世界

异步编程是一种做法:把耗时操作(例如读文件、请求服务器、访问数据库)放到后台线程里执行,而主线程继续工作:服务用户、接收新请求、响应事件。

也就是说,你下单(启动任务)后就去忙自己的事;等咖啡好了(任务完成),只需有人告诉你:“好了!”

在 Java 中,在 CompletableFuture 出现之前,这件事并不那么顺手。我们来看看它是如何发展的。

2. 历史做法:Future 及其局限

在 Java 5 中引入了接口 Future——把异步任务变得稍微方便一些的第一次尝试。它允许把任务交给线程池,并在某个时刻取得结果。

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> 2 + 2);
int result = future.get(); // 注意:在任务完成之前线程会阻塞!

想法看起来不错,但在实践中,Future 更像一个老式信箱:你把信寄出去了,可要知道有没有回信,就得不停去打开看看。

它不会在结果就绪时通知你,不支持“先做这个,然后做那个”这样的操作链,也不便于优雅地处理错误。一切最终还是落到那个会阻塞的 get() 上,异步又被拉回了“等待”。

3. CompletableFuture 的出现:异步的新风格

在 Java 8 中,真正的异步英雄取代了过时的 Future——CompletableFuture。这个类位于 java.util.concurrent 包中,成为大家写异步代码的通用利器:不再需要“手动等待”,而是可以用更优雅、简洁、易懂的方式来写。

CompletableFuture 几乎无所不能。它可以在其他线程中启动任务,把任务串成链——例如先计算结果,再处理它,接着再做点别的。它还能轻松组合多个任务:要么等待全部完成,要么只取第一个完成的。错误处理也很优雅——无需过多 try-catch。整体风格更偏函数式:不再是枯燥的调用和等待,而是使用富有表现力的方法,如 thenApplythenAccept 等。

flowchart LR
    A[异步启动任务] --> B[处理结果]
    B --> C[后续操作]
    C --> D[错误处理]

因此,CompletableFuture 把异步从“繁重的苦力活”变成了一个方便灵活的工具,让代码终于能自由呼吸。

4. 最简单的示例:迈入 CompletableFuture 的第一步

我们来看一个最小的异步任务示例:

import java.util.concurrent.CompletableFuture;

public class AsyncDemo {
    public static void main(String[] args) {
        // 异步启动任务
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 2 + 2);

        // 获取结果(会阻塞线程!)
        try {
            int result = future.get();
            System.out.println("结果: " + result); // 4
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这段代码已经在单独线程中执行计算——在启动任务的那一刻主线程并不会被阻塞。但对结果调用 get() 仍然会阻塞线程,直到结果准备好。

那如何不阻塞线程?

很简单:使用回调方法,在任务完成时再触发:

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 2 + 2);

future.thenAccept(result -> System.out.println("结果: " + result));
System.out.println("我没有被阻塞,还可以做其他事情!");

小结:

  • thenAccept —— 就是“订阅结果”:当任务完成时,执行这段代码。
  • 主线程不会等待任务完成,而是继续工作。

可视化(事件伪代码)

[主线程] --> [启动任务]
     |                   |
     v                   v
[做点别的]         [后台线程计算 2+2]
     |                   |
     v                   v
[打印 "我没有被阻塞..."]
     |                   |
     v                   v
                 [计算完成时 — 触发 thenAccept]

5. 在应用中是什么样?

设想你在开发一个控制台应用,用户可以触发数据加载(比如从数据库或服务器),而在数据加载期间——程序不会“卡住”,还能继续接受命令。

示例:模拟耗时操作

import java.util.concurrent.CompletableFuture;

public class AsyncApp {
    public static void main(String[] args) {
        System.out.println("开始加载数据...");

        CompletableFuture<String> dataFuture = CompletableFuture.supplyAsync(() -> {
            // 模拟耗时加载
            try {
                Thread.sleep(2000); // 2 秒
            } catch (InterruptedException e) {
                return "加载出错";
            }
            return "数据加载成功!";
        });

        // 订阅结果
        dataFuture.thenAccept(result -> System.out.println("结果: " + result));

        // 程序继续工作
        System.out.println("数据加载期间,我还可以做别的事情!");

        // 为了避免程序过早结束(仅用于演示!)
        try {
            Thread.sleep(2500);
        } catch (InterruptedException ignored) {}
    }
}

控制台输出将会是:

开始加载数据...
数据加载期间,我还可以做别的事情!
[2 秒后]
结果: 数据加载成功!

6. 一些有用的细节

聊聊底层的线程

当你写 CompletableFuture.supplyAsync(...) 时,任务默认会在所谓的 ForkJoinPool 中执行——这是 Java 用于并行任务的专用线程池。如果你需要更多控制(例如自定义 ExecutorService),可以把它作为第二个参数传入:

ExecutorService executor = Executors.newFixedThreadPool(2);
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 2 + 2, executor);

但对简单任务,默认线程池通常就够用了。

获取结果:get(), join(), thenAccept

  • get() —— 在结果准备好之前会阻塞线程(并抛出受检异常)。
  • join() —— 也会阻塞,但抛出的是非受检异常(RuntimeException)。
  • thenAccept()thenApply() 等 —— 不会阻塞,而是在结果就绪时调用你传入的函数。

在真实的异步应用中,请尽量避免 在主线程里使用 get()/join()

7. 初学 CompletableFuture 的常见错误

错误 1:在主线程中使用 get() 或 join()。
这样会再次阻塞程序,失去异步的优势。请改用 thenAcceptthenApply 等方法来处理结果。

错误 2:忘记处理错误。
如果异步任务中抛出了异常,它不会“冒泡”到主线程。若不通过 exceptionallyhandle 处理,你可能根本不知道哪里出了问题。

错误 3:没有等到程序结束。
在演示示例中,经常需要通过 Thread.sleep 暂停一下 main 线程——否则程序会在任务完成之前就退出。在真实应用(例如 Web 服务器)中这不是问题,但在控制台演示时要注意。

错误 4:混淆 thenAccept 和 thenApply。
thenAccept —— 用于“副作用”(不返回结果),thenApply —— 用于转换结果(返回新结果)。

错误 5:不必要地把异步与同步代码混在一起。
如果已经开始用异步,就不要再通过 get()/join() 把它拉回同步,除非是极端情况(例如在测试中)。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION