我对 Loom 项目非常感兴趣,但有一件事我无法完全理解。
\n大多数 Java 服务器使用具有一定线程限制(200、300 ..)的线程池,但是,您不受操作系统的限制可以产生更多线程,我已经读到,通过针对 Linux 的特殊配置,您可以达到巨大的数量。
\n操作系统线程成本更高,启动/停止速度更慢,必须处理上下文切换(按数量放大),并且您依赖于操作系统,而操作系统可能拒绝为您提供更多线程。
\n话虽如此,虚拟线程也消耗类似数量的内存(或者至少我是这么理解的)。使用 Loom,我们可以进行尾部调用优化,这应该会减少内存使用。另外,同步和线程上下文复制应该仍然是一个类似大小的问题。
\n事实上,您可以生成数百万个虚拟线程
\npublic static void main(String[] args) {\n for (int i = 0; i < 1_000_000; i++) {\n Thread.startVirtualThread(() -> {\n try {\n Thread.sleep(1000);\n } catch (Exception e) {\n e.printStackTrace();\n }\n });\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n当我使用平台线程时,上面的代码在 25k 左右中断并出现 OOM 异常。
\n我的问题是,到底是什么让这些线程如此轻量,是什么阻止我们生成 100 万个平台线程并使用它们,是否只是上下文切换使常规线程如此“重”。
\n一个非常相似的问题
\n到目前为止我发现的事情:
\n在这里提供一些背景信息,我一直在关注项目织机一段时间。我看过织机的状态。我做过异步编程。
异步编程(由 java nio 提供)在任务等待时将线程返回到线程池,并且它竭尽全力不阻塞线程。这带来了很大的性能提升,我们现在可以处理更多的请求,因为它们不受操作系统线程数量的直接限制。但我们在这里失去的是上下文。同一个任务现在不仅仅与一个线程相关联。一旦我们将任务与线程分离,所有上下文都将丢失。异常跟踪不提供非常有用的信息并且调试很困难。
随之而来的项目织机virtual threads成为单一的并发单元。现在您可以在单个virtual thread.
直到现在一切都很好,但文章继续指出,项目织机:
一个简单的、同步的 Web 服务器将能够处理更多的请求,而无需更多的硬件。
我不明白我们如何通过异步 API 的项目机获得性能优势?在asynchrounous APIs确保不要保留任何线程空闲。那么,项目织机如何使其比asynchronousAPI更高效和高性能?
让我重新表述这个问题。假设我们有一个 http 服务器,它接收请求并使用支持的持久数据库执行一些 crud 操作。比如说,这个 http 服务器处理了很多请求 - 100K RPM。两种实现方式:
virtual threads为每个请求生成。如果有IO,虚拟线程只是等待任务完成。然后返回 HTTP 响应。基本上,没有针对virtual threads.鉴于硬件和吞吐量保持不变,在响应时间或处理更多吞吐量方面,任何一种解决方案会比另一种更好吗?
我的猜测是,与性能没有任何区别。
Java 19 中引入了虚拟线程JEP-425作为预览功能。
在对 Java 虚拟线程(Project Loom)的概念(有时称为轻量级线程(有时称为纤维或绿色线程))进行一些研究之后,我对它们与反应式库的潜在使用非常感兴趣,例如基于 Spring WebFlux在 Project Reactor(反应流实现)和 Netty 上,用于有效地进行阻塞调用。
如今,大多数 JVM 实现都将 Java 线程实现为操作系统线程的瘦直接包装器,有时称为重量级、操作系统管理的线程平台线程。
虽然平台线程一次只能执行一个线程,但是当当前执行的虚拟线程进行阻塞调用(例如网络、文件系统、数据库调用)时,虚拟线程能够切换到执行不同的虚拟线程。
因此,在 Reactor 中处理阻塞调用时,我们使用以下构造:
Mono.fromCallable(() -> {
return blockingOperation();
}).subscribeOn(Schedulers.boundedElastic());
Run Code Online (Sandbox Code Playgroud)
我们subcribeOn()提供了一个Scheduler创建专用线程来执行该阻塞操作的方法。然而,这意味着线程最终将被阻塞,因此,由于我们仍然使用老式的线程模型,我们实际上会阻塞平台线程,这仍然不是处理 CPU 资源的真正有效的方式。
所以,问题是,我们是否可以直接使用具有反应式框架的虚拟线程来进行这样的阻塞调用,例如使用Executors.newVirtualThreadPerTaskExecutor():
创建一个执行器,为每个任务启动一个新的虚拟线程。Executor创建的线程数量是无限制的。
Mono.fromCallable(() -> {
return …Run Code Online (Sandbox Code Playgroud) java spring-boot project-reactor spring-webflux project-loom
我正在使用 Java Corretto 21.0.0.35.1 build 21+35-LTS和内置 Java HTTP 客户端来检索InputStream. 我正在使用虚拟线程发出并行请求,并且在大多数情况下,它运行良好。然而,有时,我的测试会遇到“固定”事件,如下面的堆栈跟踪所示。
我相信 JDK 已经更新为完全支持虚拟线程,并且根据我的理解,HTTP 客户端根本不应该固定承载线程。但是,似乎在读取并(自动)关闭InputStream.
这是预期的行为吗?或者它仍然是 JDK 中的一个错误吗?
代码:
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
try (InputStream responseBody = response.body()) {
return parser.parse(responseBody); // LINE 52 in the trace below
}
Run Code Online (Sandbox Code Playgroud)
踪迹
* Pinning event captured:
java.lang.VirtualThread.parkOnCarrierThread(java.lang.VirtualThread.java:687)
java.lang.VirtualThread.park(java.lang.VirtualThread.java:603)
java.lang.System$2.parkVirtualThread(java.lang.System$2.java:2639)
jdk.internal.misc.VirtualThreads.park(jdk.internal.misc.VirtualThreads.java:54)
java.util.concurrent.locks.LockSupport.park(java.util.concurrent.locks.LockSupport.java:219)
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(java.util.concurrent.locks.AbstractQueuedSynchronizer.java:754)
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(java.util.concurrent.locks.AbstractQueuedSynchronizer.java:990)
java.util.concurrent.locks.ReentrantLock$Sync.lock(java.util.concurrent.locks.ReentrantLock$Sync.java:153)
java.util.concurrent.locks.ReentrantLock.lock(java.util.concurrent.locks.ReentrantLock.java:322)
sun.nio.ch.SocketChannelImpl.implCloseNonBlockingMode(sun.nio.ch.SocketChannelImpl.java:1091)
sun.nio.ch.SocketChannelImpl.implCloseSelectableChannel(sun.nio.ch.SocketChannelImpl.java:1124)
java.nio.channels.spi.AbstractSelectableChannel.implCloseChannel(java.nio.channels.spi.AbstractSelectableChannel.java:258)
java.nio.channels.spi.AbstractInterruptibleChannel.close(java.nio.channels.spi.AbstractInterruptibleChannel.java:113)
jdk.internal.net.http.PlainHttpConnection.close(jdk.internal.net.http.PlainHttpConnection.java:427)
jdk.internal.net.http.PlainHttpConnection.close(jdk.internal.net.http.PlainHttpConnection.java:406)
jdk.internal.net.http.Http1Response.lambda$readBody$1(jdk.internal.net.http.Http1Response.java:355)
jdk.internal.net.http.Http1Response$$Lambda+0x00007f4cb5e6c438.749276779.accept(jdk.internal.net.http.Http1Response$$Lambda+0x00007f4cb5e6c438.749276779.java:-1)
jdk.internal.net.http.ResponseContent$ChunkedBodyParser.onError(jdk.internal.net.http.ResponseContent$ChunkedBodyParser.java:185)
jdk.internal.net.http.Http1Response$BodyReader.onReadError(jdk.internal.net.http.Http1Response$BodyReader.java:677)
jdk.internal.net.http.Http1AsyncReceiver.checkForErrors(jdk.internal.net.http.Http1AsyncReceiver.java:302)
jdk.internal.net.http.Http1AsyncReceiver.flush(jdk.internal.net.http.Http1AsyncReceiver.java:268)
jdk.internal.net.http.Http1AsyncReceiver$$Lambda+0x00007f4cb5e31228.555093431.run(jdk.internal.net.http.Http1AsyncReceiver$$Lambda+0x00007f4cb5e31228.555093431.java:-1)
jdk.internal.net.http.common.SequentialScheduler$LockingRestartableTask.run(jdk.internal.net.http.common.SequentialScheduler$LockingRestartableTask.java:182)
jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.java:149)
jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.java:207)
jdk.internal.net.http.HttpClientImpl$DelegatingExecutor.execute(jdk.internal.net.http.HttpClientImpl$DelegatingExecutor.java:177)
jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(jdk.internal.net.http.common.SequentialScheduler.java:282)
jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(jdk.internal.net.http.common.SequentialScheduler.java:251)
jdk.internal.net.http.Http1AsyncReceiver.onReadError(jdk.internal.net.http.Http1AsyncReceiver.java:516)
jdk.internal.net.http.Http1AsyncReceiver.lambda$handlePendingDelegate$3(jdk.internal.net.http.Http1AsyncReceiver.java:380)
jdk.internal.net.http.Http1AsyncReceiver$$Lambda+0x00007f4cb5e33ca0.84679411.run(jdk.internal.net.http.Http1AsyncReceiver$$Lambda+0x00007f4cb5e33ca0.84679411.java:-1) …Run Code Online (Sandbox Code Playgroud) 我正在研究Project Loom 的运作方式以及它能为我的公司带来什么样的好处。
\n所以我理解其动机,对于基于标准 servlet 的后端,总是有一个执行业务逻辑的线程池,一旦线程因为 IO 而被阻塞,它除了等待之外什么也做不了。假设我有一个具有单个端点的后端应用程序,该端点背后的业务逻辑是使用 JDBC 读取一些数据,该 JDBC 内部使用 InputStream,后者将再次使用阻塞系统调用(就 Linux 而言,为 read())。因此,如果我有 20000 个用户到达此端点,我需要创建 200 个线程,每个线程等待 IO。
\n现在假设我将线程池切换为使用虚拟线程。根据 Ben Evans 在《深入 Java\xe2\x80\x99s Project Loom 和虚拟线程》一文中的说法:
\n\n\n相反,当进行阻塞调用(例如 I/O)时,虚拟线程会自动放弃(或让出)其承载线程。
\n
据我了解,如果我的操作系统线程数量等于 CPU 核心数量和无限数量的虚拟线程,则所有操作系统线程仍将等待 IO,并且执行程序服务将无法为虚拟分配新工作线程,因为没有可用的线程来执行它。它与常规线程有何不同,至少对于操作系统线程,我可以将其扩展到数千以增加吞吐量。或者我只是误解了 Loom 的用例?提前致谢
\n我刚刚读过这个邮件列表:
\n\n\n虚拟线程喜欢阻塞 I/O。如果线程需要阻塞(例如 Socket 读取),那么这会释放底层内核线程以执行其他工作
\n
我不确定我是否理解它,如果操作系统执行诸如读取之类的阻塞调用,则操作系统无法释放线程,出于这些目的,内核具有非阻塞系统调用,例如 epoll,它不会阻塞线程并立即返回具有一些可用数据的文件描述符列表。上面的引用是否意味着在幕后,如果调用它的线程是虚拟的, JVM 会将阻塞替换read为非阻塞?epoll
我已对此进行了编辑,以使原始帖子的内容保持最新。
我想尝试JEP 428:结构化并发(孵化器)中定义的新Project Loom功能
我的 pom.xml 中有
<properties>
<maven.compiler.executable>${env.JAVA_HOME}/bin/javac</maven.compiler.executable>
<maven.compiler.source>19</maven.compiler.source>
<maven.compiler.target>19</maven.compiler.target>
</properties>
. . .
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<compilerArgs>
<arg>--add-modules=jdk.incubator.concurrent</arg>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
Run Code Online (Sandbox Code Playgroud)
其中JAVA_HOME指向 JDK 19,但是当我尝试通过构建时,mvn compile我得到
[ERROR] C:\Users\ERIC\Documents\git\loom-lab\laboratory\src\main\java\net\kolotyluk\loom\Structured.java:3:20: error: package jdk.incubator.concurrent is not visible
[ERROR] C:\Users\ERIC\Documents\git\loom-lab\laboratory\src\main\java\net\kolotyluk\loom\Structures.java:3:20: error: package jdk.incubator.concurrent is not visible
. . .
Run Code Online (Sandbox Code Playgroud)
很多人在这方面帮助了我,显然他们可以让它发挥作用,但由于某种原因,我无法开始mvn compile工作。
不过,我可以让代码在 IntelliJ 下编译和运行。当我可以让 IntelliJ 编译时,我从来没有无法让 Maven 编译。通常,情况恰恰相反。
JDK 开发人员建议永远不要池化虚拟线程,因为创建和销毁虚拟线程的成本非常低。我对池的想法有点困惑,因为池通常意味着两件事:
我知道 JDK 开发人员希望我们永远不要重用虚拟线程,而生命周期问题让我感到困惑,因为如果有多个虚拟线程的生命周期与应用程序本身一样长,那么听起来可能像是没有重用的池化。
那么,虚拟线程是否应该快速死亡,或者具有较短的有界生命周期,或者多个虚拟线程阻塞、偶尔被唤醒以处理某些任务并且具有非常长的生命周期是否可以?
例如,是否可以使用 RecursiveAction 与虚拟线程池(而不是 fork/join 池)结合使用(在我尝试设计不良的自定义工作之前)?
我们有一个 Jetty Web 应用程序,带有由 Java19 虚拟线程支持的自定义线程池。
\n我们响应请求而运行的业务逻辑通常是 IO 密集型的(例如数据库查询),因此虚拟线程对我们来说是一个巨大的胜利,它允许我们同时处理更多的 IO 密集型请求。可以使用平台线程,同时避免显式编写异步代码。
\n但我们的一些请求具有受 CPU 限制的计算部分。如果有足够多的请求恰好同时运行 CPU 密集型代码,我们的整个 Web 应用程序将锁定并对新请求不再响应,直到其中一个请求得到解决。
\nJava19 的虚拟线程支持显然是通过将所有虚拟线程调度到由 N 个底层承载线程支持的单个 JVM 全局有限大小的 ForkJoinPool 上来实现的。
\n这意味着,如果我启动许多虚拟线程 \xe2\x80\x94 并且这些线程中至少 N 个有一些长时间运行的 CPU 密集型操作作为其中的一部分 \xe2\x80\x94 那么一旦这 N 个线程到达对于这个 CPU 密集部分,整个 JVM 全局虚拟线程池将被锁定/阻塞,因为所有可用的载体线程都被运行 CPU 密集代码的虚拟线程之一使用。
\n如果应用程序具有像这样的混合 IO 密集型/CPU 密集型工作负载,那么在设计使用虚拟线程作为顶级并发机制的应用程序时,最佳实践是什么?
\n我确实知道平台线程很昂贵,因为它需要更多内存并且容易发生 CPU 上下文切换。
但是,在虚拟线程的情况下,少数平台线程可以服务难以想象的大量虚拟线程,虚拟线程是否仍然需要内存空间来钝化上下文/堆栈,然后将其附加到载体线程?
它对记忆有何影响?
为什么自旋 10000 个虚拟线程不会因内存不足而消亡,而 10000 个平台线程则会因内存不足而消亡?
他们都需要相同的堆栈吗?以及需要维护应用程序相关信息的上下文,对吧?
内存中是否存在仅适用于平台线程的额外开销,这就是我们说虚拟线程在内存中“更轻”的原因?如果是的话,是什么造成了这种差异?
java ×10
project-loom ×10
java-21 ×3
asynchronous ×1
concurrency ×1
fork-join ×1
java-19 ×1
java-loom ×1
spring-boot ×1