随着 Java 21 的发布,虚拟线程被引入作为可完成的 future 和线程的替代并发模型。可完成的 future 和虚拟线程都旨在简化 Java 中的异步编程,但采取了不同的方法。本文探讨了两者之间的主要区别。
什么是CompletableFutures?
CompletableFuture 是 Java 8 中添加的一个类,表示可以产生结果或异常完成的异步计算。它实现了 Future 接口,并提供了手动完成 future、等待完成、注册回调以及将可计算阶段链接在一起的方法。
CompletableFuture 的主要特点:
- 异步操作——等待结果时不会阻塞主线程。
- 回调——可以注册回调以在未来完成时执行。
- 链式处理 - future 可以链接在一起以管道异步步骤。
- 异常处理——异常是通过回调显式处理的。
- 线程池——默认在 ForkJoinPool 公共线程池上运行计算。
CompletableFutures 通过将线程管理与应用程序逻辑解耦来实现异步编程。然而,它们仍然会遇到诸如回调地狱之类的问题,并且使用起来并不总是直观的。
用法示例:
// Run two async tasks
CompletableFuture future1 = CompletableFuture.supplyAsync(() -> {
// long running task
return "Result1";
});
CompletableFuture future2 = CompletableFuture.supplyAsync(() -> {
// long running task
return "Result2";
});
// Join tasks and handle results
future1.thenCombine(future2, (res1, res2) -> {
return res1 + " " + res2;
}).thenAccept(System.out::println);
CompletableFuture 易于使用,并且可以很好地扩展,适合中等数量的异步任务。但是,如果创建太多实例,可能会产生开销。
什么是虚拟线程?
Java 14 中引入了虚拟线程作为异步编程的替代方法。关键思想是,我们可以切换由 Java 运行时自动映射到真实线程的虚拟线程,而不是显式管理线程池。
虚拟线程的主要特点:
- 轻量级——与操作系统线程相比,内存和资源开销较低。
- 托管阻塞——阻塞操作不会阻塞操作系统线程,从而将它们释放以执行其他任务。
- 结构化并发——虚拟线程从父线程继承上下文。
- 异步——不需要基于回调的代码。可以使用同步代码风格。
- 优化的扩展——可以有效地启动数千个虚拟线程。
个例子:
public class Main {
public static void main(String[] args) {
VirtualThread vt = VirtualMachine.getInstance().virtualThread(
() -> {
// long running task
}
);
vt.start();
// can continue executing main thread logic
}
}
虚拟线程旨在提供更简单的线程模型,可以更好地扩展并避免诸如回调地狱之类的问题。编程模型更像是同步代码,但执行是异步的。
主要差异
可完成的 future 和虚拟线程之间的一些主要区别:
- 风格 - CompletableFuture 具有基于回调的风格,而虚拟线程允许异步逻辑的同步编码风格。
- 阻塞——虚拟线程上的阻塞操作不会像可完成的未来那样消耗操作系统线程。
- 开销——虚拟线程的资源开销比可完成的 future 使用的线程/线程池低。
- 结构化并发——虚拟线程从父线程继承上下文,而可完成的 future 没有内置的父子关系概念。
- 链接——CompletableFuture 可以更轻松地将多个未来阶段直接链接在一起。使用虚拟线程链接逻辑需要显式代码。
- 错误处理——虚拟线程上未处理的异常会终止线程,而可完成的 future 需要通过回调进行处理。
总结:
- CompletableFuture 是高级的、灵活的,但会产生开销。对于编写许多小任务很有用。
- 虚拟线程是轻量级的并提供结构化并发。更适合较少的长时间运行任务。
两者都有其优点和用例,但虚拟线程代表了 Java 平台上异步编程的演变。两者之间的选择取决于应用程序要求和首选编程风格