[关闭]
@levinzhang 2023-01-03T14:08:34.000000Z 字数 3357 阅读 588

如何在Spring中启用虚拟线程

摘要

JDK 19中引入了对虚拟线程的支持,尽管该特性依然处于预览阶段,但是鉴于它可能会对Java并发编程产生巨大影响,所以Spring框架在第一时间介绍了对虚拟线程的支持情况。


本文最初发表于Spring网站,经原作者Mark Paluch授权,由InfoQ中文站翻译分享。

Loom项目已经通过JEP 425进入到了JDK中。从2022年9月份开始,它作为一项预览功能对外提供。它的目标是大幅度减少编写、维护和观测高吞吐并发应用相关的工作。

虚拟线程的适用场景

对于应用开发人员和Spring框架来说,轻量级虚拟线程是一种令人兴奋的方式。过去几年,有趋势表明应用程序更加倾向于通过网络进行相互通信。很多应用会使用数据存储、消息代理和远程服务。如果应用基于阻塞式的I/O基础设施来构建的话,比如使用了InputStream以及同步的HTTP、数据库和消息代理客户端,那么这种I/O密集型的应用是虚拟线程的主要受益对象。与平台线程相比,在虚拟线程上运行这样的工作负载能够减少内存占用,在某些场景下,虚拟线程能够增加并发性。

如果系统尚有并发所需的额外资源,那么就可以实现更高的并发性。具体来讲,这些资源包括:

  1. 连接池中的可用连接
  2. 足够的内存以服务于增加的负载
  3. 未使用的CPU时间

虚拟线程的使用显然并不局限于减少内存占用或增加并发性。虚拟线程的引入还促使人们更广泛地审视在只能使用平台线程的时代所做出的决策。

并发工具的修订

如果没有配置ThreadFactory的话,Spring框架的SimpleAsyncTaskExecutor会为提交的每个可运行任务使用一个新的平台线程。这样的安排需要创建平台线程,从而导致更低的吞吐量和更高的内存消耗。SimpleAsyncTaskExecutor可以进行修改,以使用虚拟线程,这样在默认配置下,可以减少内存占用并增加吞吐量。(同时,可以使用一个自定义的TaskExecutor变种达到相同的效果。)

编程模型的修订

虚拟线程可以改变我们对异步编程接口的看法。如果假设我们的代码从一开始就可以在虚拟线程上运行,那么在很多情况下使用异步编程模型的理由将不复存在。虚拟线程的分配更加轻量级,线程数量不再是可扩展性的主要限制。更具体来说,异步编程模型并没有消除像网络调用这样的延迟。如果网络调用无法进行,异步的Apache HTTP客户端或Netty只是对任务进行切换,而不会阻塞线程。虚拟线程也是同样的情况,它们实际上会将线程让位于一个可以继续运行的Runnable

Loom项目已经重新修订了Java运行时库中所有可能阻塞的区域,并更新了代码,以便在遇到阻塞时,进行虚拟线程的切换。Java的并发工具(如ReentrantLockCountDownLatchCompletableFuture等)都可以在虚拟线程上使用,而不会阻塞底层的平台线程。这一变化使得Future.get().get(Long, TimeUnit)都能在虚拟线程中良好运行,而且消除了使用回调驱动的Future的必要性。

随着虚拟线程的引入,采用异步Servlet API的假设可能会失效。异步Servlet API的引入是为了释放服务器线程,这样服务器就可以继续为请求提供服务,而工作者线程会继续在原有的请求上运行。在虚拟线程上运行servlet请求和响应处理的话,就不需要释放服务器线程了,这就导致了一个问题:异步fork线程会涉及大量的状态的保存,而虚拟线程根本不需要这样做,那为什么还要使用ServletRequest.startAsync()呢?

迁移的限制

我们的团队从虚拟线程被称为Fibers的时候就开始尝试使用它。从那时一直到Java 19发布,有一个限制一直存在,那就是使用synchronized时,会导致平台线程被锚定(pin),这会降低并发性。使用synchronized本身并不是什么问题,只有当这些代码块中包含阻塞代码时(一般来讲是I/O操作)这样做才是有问题的。这种安排可能是有问题的,因为载体平台线程是一种有限的资源,当虚拟线程运行的代码锚定到平台线程时,如果不对工作负载进行仔细地检查,将会导致应用性能的降低。事实上,即便没有虚拟线程,在同步代码块中阻塞代码也会导致性能问题。

Spring框架大量使用了synchronized来实现锁定,但是它主要针对的是本地数据结构。多年来,在虚拟线程出现之前,我们已经对可能与第三方资源交互的synchronized代码块进行了修订,移除了高并发应用中的锁竞争。所以,由于Spring拥有庞大的社区和来自现有并发应用的广泛反馈,Spring框架的状况已经相当不错了。在成为虚拟线程场景中最佳公民的道路上,我们会进一步审视I/O或其他阻塞代码上下文中的synchronized使用,以避免平台线程被锚定在热点代码路径上,从而使应用能够从Loom项目中获取最大的收益。

在虚拟线程上运行Spring应用

有了最新版本的Spring框架、Spring Boot和Apache Tomcat之后,你就可以自行体验虚拟线程了。首先,你需要分析虚拟线程会如何影响你的应用,并针对虚拟线程和平台线程进行基准测试。要自定义Spring Boot应用以使用虚拟线程处理请求,你只需添加如下的自定义配置。

  1. @Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
  2. public AsyncTaskExecutor asyncTaskExecutor() {
  3. return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
  4. }
  5. @Bean
  6. public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
  7. return protocolHandler -> {
  8. protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
  9. };
  10. }

目前,我们正在尽一切努力使预览版的体验可以无缝实现,我们期望一旦Loom项目在新的OpenJDK中结束预览阶段,就能提供最佳的配置方案。

如果我们得知核心框架中存在面向虚拟线程的明确优化潜力,无论是synchronized的使用还是ThreadLocal的使用,我们都会尽可能在即将发布的Spring框架和Spring Boot维护版本中推出相应的改进措施,甚至在Loom正式发布之前。

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

你的应用如何从虚拟线程中受益?

这是一个比会不会带来好处更具体的问题,也是一个更难回答的问题。

可以说,最可能的情况是,如果你目前根本没有做任何异步的事情(甚至没有使用Servlet 3.1风格的异步请求,否则的话你可能需要做一些修改以更好地保持一致性),那么你可以在几乎不做任何变更的情况下受益。当然,为了获取收益,需要有一些适用于Loom的实际I/O或其他线程park操作。

我们也相信,ReactiveX风格的API仍然是组合并发逻辑的强大方式,也是处理流的自然方式。我们认为虚拟线程是对反应式编程模型的补充,可以消除阻塞式I/O的障碍,而单纯使用虚拟线程来处理无限流仍然是一个挑战。在声明性并发(如散点收集)很重要的场景中,ReactiveX依然是正确的并发方式。其底层的Reactive Streams规范为数据流水线的需求、回压(back pressure)和取消(cancellation)定义了协议,它不局限于非阻塞式API或特定的Thread的使用。

我们非常期待来自应用的经验和反馈。我们目前的重点是确保你能够自行开始体验虚拟线程的特性。如果你在自己的早期实验中遇到虚拟线程的具体问题,请向相应的项目报告。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注