拥抱虚拟线程

工程 | Mark Paluch | 2022 年 10 月 11 日 | ...

Project Loom 已经通过 JEP 425 进入了 JDK。自 2022 年 9 月的 Java 19 版本起,它作为预览功能可用。其目标是大幅降低编写、维护和观察高吞吐量并发应用程序的难度。

虚拟线程适用场景

这使得轻量级的虚拟线程成为应用程序开发人员和 Spring Framework 的一种令人兴奋的方法。过去几年表明,应用程序之间通过网络通信的趋势越来越明显。许多应用程序使用数据存储、消息代理和远程服务。如果 I/O 密集型应用程序构建为使用阻塞式 I/O 功能(例如 InputStream 和同步 HTTP、数据库和消息代理客户端),那么它们将主要受益于虚拟线程。在虚拟线程上运行此类工作负载有助于降低与平台线程相比的内存占用,并且在某些情况下,虚拟线程可以提高并发性。

如果系统具有并发所需的额外资源,则可以实现更高的并发性。具体来说,这些是

  1. 连接池中的可用连接

  2. 足够的服务增加负载的内存

  3. 未使用的 CPU 时间

虚拟线程的使用显然不仅限于直接减少内存占用或提高并发性。虚拟线程的引入也促使人们重新审视在仅有平台线程可用时为运行时做出的决策。

并发实用程序的修订

如果未配置 ThreadFactory,则 Spring Framework 的 SimpleAsyncTaskExecutor 会为提交的每个 Runnable 使用一个新的平台线程。这种安排需要创建平台线程,从而导致吞吐量降低和内存消耗增加。SimpleAsyncTaskExecutor 可以修改为默认使用虚拟线程,以减少其默认配置中的内存占用并提高吞吐量。(同时,可以使用自定义的 TaskExecutor 变体来达到相同的效果。)

编程模型的修订

虚拟线程可以改变我们对异步编程接口的看法。如果我们从代码在虚拟线程上运行的假设开始,那么在许多情况下,使用异步编程模型的原因就会消失。虚拟线程的分配更加轻量级,线程数量不再是可扩展性的主要限制因素。为了使事情更清楚,异步编程模型并不能消除例如网络调用的延迟。异步 Apache HTTP 客户端或 Netty 只是在网络调用无法继续时切换任务,而不是阻塞线程。虚拟线程也发生同样的情况:它们有效地让位于另一个可以继续工作的 Runnable

Project Loom 重新审视了 Java 运行时库中所有可能阻塞的区域,并更新了代码以在代码遇到阻塞时让出。Java 的并发实用程序(例如 ReentrantLockCountDownLatchCompletableFuture)可以在虚拟线程上使用而不会阻塞底层平台线程。此更改使 Future.get().get(Long, TimeUnit) 成为虚拟线程上的良好公民,并消除了对基于回调的 Future 使用的需要。

随着虚拟线程的引入,导致异步 Servlet API 的假设可能会失效。引入异步 Servlet API 的目的是释放服务器线程,以便服务器可以在工作线程继续处理请求时继续服务请求。在虚拟线程上运行 servlet 请求和响应处理消除了释放服务器线程的需要,从而引出了一个问题,为什么要使用 ServletRequest.startAsync(),因为异步分叉涉及大量状态保存,这些状态保存可以消除,因为它不再需要。

缓解限制

我们的团队从虚拟线程被称为 Fibers 时就开始尝试使用它们。从那时起,直到 Java 19 发布,一个限制一直很普遍,导致平台线程固定,在使用 synchronized 时有效地降低了并发性。synchronized 代码块的使用本身并不是问题;通常来说,只有当这些块包含阻塞代码(通常是 I/O 操作)时才会出现问题。这些安排可能存在问题,因为承载平台线程是一种有限的资源,并且在没有仔细检查工作负载的情况下,在虚拟线程上运行代码时,平台线程固定会导致应用程序性能下降。事实上,即使没有虚拟线程,同步块中的相同阻塞代码也会导致性能问题。

Spring Framework 大量使用 synchronized 来实现锁定,主要是在本地数据结构周围。多年来,在虚拟线程可用之前,我们已经修改了可能与第三方资源交互的 synchronized 块,消除了高并发应用程序中的锁争用。因此,由于其庞大的社区和来自现有并发应用程序的大量反馈,Spring 已经处于非常好的状态。在成为虚拟线程场景中尽可能优秀的公民的道路上,我们将在 I/O 或其他阻塞代码的上下文中进一步重新审视 synchronized 的使用,以避免在热点代码路径中发生平台线程固定,以便您的应用程序能够充分利用 Project Loom。

在虚拟线程上运行 Spring 应用程序

使用最新版本的 Spring Framework、Spring Boot 和 Apache Tomcat,您可以开始自己尝试。您可以开始分析虚拟线程如何影响您的应用程序工作负载,并对虚拟线程使用与平台线程使用进行基准测试。要自定义您的 Spring Boot 应用程序以在虚拟线程上处理 servlet 请求,请应用以下自定义

@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor asyncTaskExecutor() {
  return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}

@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
  return protocolHandler -> {
    protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
  };
}

目前,我们正在尽一切努力使预览体验尽可能流畅,并且我们预计一旦 Loom 在新的 OpenJDK 版本中退出预览,我们将提供一流的配置选项。

如果我们了解到核心框架中面向虚拟线程的优化的具体潜力,无论是某些 synchronized 使用点还是某些 ThreadLocal 使用,我们都会尽力将相应的改进整合到即将发布的 Spring Framework 和 Spring Boot 维护版本中,甚至在 Loom 正式发布之前。

虚拟线程不仅会影响 Spring Framework,还会影响所有周围的集成,例如数据库驱动程序、消息传递系统、HTTP 客户端等等。许多这些项目都意识到需要改进其 synchronized 行为以释放 Project Loom 的全部潜力。

您的应用程序能否从虚拟线程中受益?

这是一个比“是否会受益”更具体的问题,而且更难回答。

我们可以说,您几乎无需任何更改即可获益的最可能场景是,您目前根本没有执行任何异步操作(甚至没有 Servlet 3.1 样式的异步请求,否则您可能需要进行一些修改以更好地对齐)。当然,Loom 必须有一些实际的 I/O 或其他线程停放才能带来好处。

我们也认为,ReactiveX 风格的 API 仍然是组合并发逻辑和处理流的强大方法。我们看到虚拟线程补充了反应式编程模型,消除了阻塞 I/O 的障碍,而使用虚拟线程纯粹处理无限流仍然是一个挑战。对于声明式并发(例如散布-收集)很重要的并发场景,ReactiveX 是正确的方法。底层的 Reactive Streams 规范定义了一种用于数据管道的需求、背压和取消的协议,而不会将其限制在非阻塞 API 或特定的线程使用上。

我们非常期待从应用程序中获得大家的经验和反馈。我们目前专注于确保您能够开始自行进行实验。如果您在自己的早期虚拟线程实验中遇到具体问题,请将其报告给相应的项目。

尝试在您基于 Spring 的应用程序中使用虚拟线程,并告诉我们结果如何!

获取 Spring 新闻通讯

与 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部