拥抱虚拟线程

工程 | 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 Client 或 Netty 在网络调用无法继续时只是切换任务,而不是阻塞线程。对于虚拟线程也是如此:它们会有效地让出给另一个可以继续工作的 Runnable

Project Loom 重新审视了 Java 运行时库中所有可能阻塞的区域,并更新了代码,以便在遇到阻塞时让出执行权。Java 的并发工具(例如 ReentrantLockCountDownLatchCompletableFuture)可以在虚拟线程上使用,而不会阻塞底层的平台线程。这一改变使得 Future.get().get(Long, TimeUnit) 在虚拟线程上成为“好公民”,并消除了使用 Future 的回调驱动方式的需求。

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

缓解局限性

自虚拟线程被称为 Fibers 以来,我们的团队一直在进行实验。从那时起直到 Java 19 发布,一个突出的局限性是使用 synchronized 时会导致平台线程固定(pinning),从而有效降低并发性。使用 synchronized 代码块本身并不是问题;问题仅在于这些代码块中包含阻塞代码,通常指的是 I/O 操作。这种安排可能存在问题,因为载体平台线程是有限的资源,在虚拟线程上运行代码而未仔细检查工作负载时,平台线程固定可能导致应用程序性能下降。实际上,即使没有虚拟线程,同步块中的相同阻塞代码也可能导致性能问题。

Spring Framework 大量使用 synchronized 来实现锁定,主要围绕本地数据结构。多年来,在虚拟线程可用之前,我们已经修订了可能与第三方资源交互的 synchronized 块,消除了高并发应用程序中的锁争用。因此,凭借其庞大的社区和来自现有并发应用程序的广泛反馈,Spring 的现状已经相当不错。为了在虚拟线程场景中成为最佳公民,我们将进一步审视 synchronized 在 I/O 或其他阻塞代码上下文中的使用,以避免在热点代码路径中发生平台线程固定,从而使您的应用程序能够最大程度地利用 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());
  };
}

我们正在尽一切努力使预览体验尽可能顺畅,并且我们希望在 Project Loom 在新的 OpenJDK 版本中脱离预览状态后提供一流的配置选项。

如果我们发现核心框架中存在虚拟线程优化(无论是某些 synchronized 使用点还是某些 ThreadLocal 使用)的具体潜力,我们将尽最大可能在即将发布的 Spring Framework 和 Spring Boot 维护版本中进行相应的改进,甚至在 Project Loom 全面可用之前。

虚拟线程不仅影响 Spring Framework,还影响所有相关的集成,例如数据库驱动程序、消息系统、HTTP 客户端等等。许多这些项目都意识到需要改进它们的 synchronized 行为,以充分发挥 Project Loom 的潜力。

您的应用程序会从虚拟线程中受益吗?

这是一个比“是否会有益处”更具体、也更难回答的问题。

我们可以说,最有可能让您几乎不做任何更改就能受益的场景是,如果您目前完全没有进行任何异步操作(即使是 Servlet 3.1 风格的异步请求也没有,否则您可能需要进行一些修订以更好地对齐)。当然,还需要有一些实际的 I/O 或其他线程挂起操作,Project Loom 才能带来好处。

我们也相信 ReactiveX 风格的 API 仍然是组合并发逻辑的强大方式,也是处理流的自然方式。我们将虚拟线程视为对响应式编程模型的补充,它们消除了阻塞式 I/O 的障碍,而仅使用虚拟线程处理无限流仍然是一个挑战。ReactiveX 是适用于声明性并发(如 Scatter-Gather)场景的正确方法。底层的 Reactive Streams 规范定义了一个关于数据管道需求、背压和取消的协议,而不局限于非阻塞 API 或特定的线程使用。

我们非常期待从各种应用程序中获得的集体经验和反馈。我们目前的重点是确保您能够开始自己的实验。如果您在早期的虚拟线程实验中遇到特定问题,请向相应的项目报告。

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

获取 Spring Newsletter

通过 Spring Newsletter 保持连接

订阅

领先一步

VMware 提供培训和认证,助您飞速发展。

了解更多

获取支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将到来的活动

查看 Spring 社区所有即将到来的活动。

查看全部