使用虚拟线程(Project Loom)与Spring WebFlux/Reactor/Reactive库。

16

Java虚拟线程

在Java 19中引入了虚拟线程JEP-425作为预览功能。

在对Java虚拟线程(项目Loom)的概念进行一些调查后,有时被称为轻量级线程(或有时称为纤程或绿色线程),我对它们与反应式库的潜在用途非常感兴趣,例如基于Project Reactor(反应式流实现)和Netty的Spring WebFlux,以便高效地进行阻塞调用。

大多数JVM实现今天将Java线程实现为对操作系统线程的薄包装,有时称为重量级、由操作系统管理的平台线程。

虚拟线程可以在当前执行的虚拟线程进行阻塞调用(例如网络、文件系统、数据库调用)时切换到执行不同的虚拟线程,而平台线程一次只能执行一个线程。

我们如何在 Reactor 中处理阻塞调用?

所以,在处理 Reactor 中的阻塞调用时,我们使用 以下结构

Mono.fromCallable(() -> {
     return blockingOperation();
}).subscribeOn(Schedulers.boundedElastic());

subcribeOn()中,我们提供了一个Scheduler,它为执行阻塞操作创建了一个专用线程。然而,这意味着该线程最终会被阻塞,因此,由于我们仍然采用传统的线程模型,实际上会阻塞平台线程,这并不是处理CPU资源的高效方式。

问题如下:

所以,问题是,我们是否可以直接在响应式框架中使用虚拟线程来进行像这样的阻塞调用,例如使用Executors.newVirtualThreadPerTaskExecutor()

创建一个为每个任务启动新的虚拟线程的Executor。 Executor创建的线程数是无限的。

Mono.fromCallable(() -> {
    return blockingOperation();
}).subscribeOn(Schedulers.fromExecutorService(Executors.newVirtualThreadPerTaskExecutor()));

能直接使用吗?我们是否能真正从这种方法中获得好处,以更高效地处理我们的CPU资源并提高应用程序的性能?这是否意味着我们可以轻松地将响应式库与任何阻塞库/框架集成,例如基于JDBC的Spring Data JPA和其他无数的库/框架,并且像魔术般地将它们转变为非阻塞的?

2
Reactor中的织布机集成存在一个未解决的问题:https://github.com/reactor/reactor-core/issues/3084 可能需要关注此问题以进行进一步的开发。 - Martin Tarjányi
2
Reactor中的织布机集成存在一个未解决的问题:https://github.com/reactor/reactor-core/issues/3084 可能需要关注此问题以进行进一步的开发。 - Martin Tarjányi
1个回答

5

在响应式代码中的阻塞

在响应式代码中,你也可以进行阻塞操作,但通常不是一个好主意。

没有虚拟线程,进行阻塞操作会阻塞平台线程,因此如果在响应式代码中进行阻塞操作,实际上就浪费了平台线程。

使用 Project Loom

如果你正在使用 Executors.newVirtualThreadPerTaskExecutor(),这个问题就不再存在(至少在大多数情况下,对于一些特殊情况,比如原生代码或者在 synchronized 块中进行阻塞操作时,虚拟线程会被“固定”到一个平台线程上)。

问题所在

问题在于你打破了范式。虽然你的项目是响应式的,但其中一部分代码却不是,导致你的代码库在某些部分使用了响应式代码,而在其他部分则没有。然而,如果你正在将现有的响应式项目逐步迁移到使用虚拟线程的同步代码,并计划从项目中移除响应式框架(可以逐步进行),那么这可能是可以接受的临时解决方案。

然而,请注意,虚拟线程(在撰写本文时)仍处于预览功能阶段,因此可能会有一些重大变化。因此,您可能不想立即迁移到虚拟线程,并等待虚拟线程退出预览,因为切换回去可能会非常困难。虚拟线程将在Java 21中退出预览。
无论如何,不要仅仅因为使用Loom的虚拟线程就在反应式代码中进行阻塞,而选择使用反应式框架。相反,要么选择反应式编程风格,要么使用虚拟线程并以同步方式编写。如果您有特定的工作需要在平台线程中完成(例如,CPU密集型工作),那么您可以创建一个平台线程来执行该工作。
毕竟,反应式编程的整个目的是不阻塞线程。如果您想阻塞(虚拟)线程,那么使用反应式框架就没有意义(至少在我看来)。
从反应式迁移到虚拟线程的方式取决于您和您的项目。如果您的项目已经适当地模块化,那么选择模块化方法并逐个迁移模块可能是一个不错的主意。
如果您有一个使用响应式框架的代码库,并且想要切换到虚拟线程,您可以通过配置响应式框架本身来使用虚拟线程,例如使用Executors.newVirtualThreadPerTaskExecutor()或类似方法。对于自上而下的方法,只要阻塞代码在虚拟线程中运行即可,无需进行此配置。
自上而下方法: 您可以尝试自上而下地迁移代码-确保调用代码在虚拟线程中执行,并逐步重写为阻塞方式。为此,您可以(由于您在虚拟线程中)运行一些响应式操作并阻塞等待操作的最终结果。然后,当没有其他响应式代码使用此操作时(所有调用该操作的代码都已重写为虚拟线程),您可以稍后继续重写被响应式操作调用的响应式代码。
自下而上方法: 响应式框架通常提供了一种使用另一个线程或类似方法运行非响应式代码的方式。您可以将其配置为使用虚拟线程,然后从底部开始逐步重写代码。
为了实现这一点,您将开始重写不依赖于其他响应式代码的响应式代码,并更改调用代码以将其视为非响应式代码(同时确保它在虚拟线程中运行)。
混合方法:
你也可以从中间开始。为此,你确实需要确保你的响应式框架在虚拟线程中运行你的代码。
然后,当调用你的代码的代码将你重写的代码视为非响应式代码时,你会阻塞被调用的响应式代码。
然而,你应该注意,这将导致破坏范式的上述问题。你可能会有一半是响应式的、一半是阻塞的代码,这可能很麻烦。
模块化
如果你对代码库进行大规模重写,可能会出现部分重写而其他部分没有重写的情况。
如果你的应用程序具有明确定义的模块(可以是微服务,但单体应用程序也可以进行模块化),你可以逐个重写一个模块。如果你正在使用微服务,你应该能够逐个重写一个微服务而不影响其他微服务。

3
免责声明:除非绝对必要,否则我拒绝使用响应式框架,因此我的观点可能有些偏见。 - dan1st

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接