5

我正在研究Project Loom是如何运作的,以及它能给我的公司带来什么样的好处。

所以我理解动机,对于基于标准 servlet 的后端,总是有一个执行业务逻辑的线程池,一旦线程因为 IO 而被阻塞,它除了等待什么也做不了。因此,假设我有一个具有单个端点的后端应用程序,该端点背后的业务逻辑是使用内部使用 InputStream 的 JDBC 读取一些数据,后者将再次使用阻塞系统调用(在 Linux 中为 read())。所以如果我有 20000 个用户到达这个端点,我需要创建 200 个线程,每个线程等待 IO。

现在假设我将线程池切换为使用虚拟线程。根据 Ben Evans 在Going inside Java's Project Loom and virtual threads一文中的说法:

相反,当进行阻塞调用(例如 I/O)时,虚拟线程会自动放弃(或让出)它们的载体线程。

据我了解,如果我的操作系统线程数量等于 CPU 内核数量和无限数量的虚拟线程,所有操作系统线程仍将等待 IO,并且执行器服务将无法为虚拟线程分配新工作因为没有可用的线程来执行它。它与常规线程有何不同,至少对于操作系统线程,我可以将其扩展到数千以增加吞吐量。还是我只是误解了 Loom 的用例?提前致谢

添加在

我刚刚阅读了这个邮件列表

虚拟线程喜欢阻塞 I/O。如果线程需要阻止 Socket 读取,那么这会释放底层内核线程以执行其他工作

我不确定我是否理解它,如果操作系统执行诸如读取之类的阻塞调用,则操作系统无法释放线程,出于这些目的,内核具有非阻塞系统调用,例如 epoll,它不会阻塞线程并立即返回有一些可用数据的文件描述符列表。上面的引用是否暗示在后台,如果调用它的线程是虚拟的,JVM 将用read非阻塞替换阻塞?epoll

4

3 回答 3

7

您的第一个摘录缺少重点:

相反,当进行阻塞调用(例如 I/O)时,虚拟线程会自动放弃(或让出)它们的载体线程。这是由库和运行时处理的[...]

含义是这样的:如果您的代码对库(例如 NIO)进行了阻塞调用,则库检测到您从虚拟线程调用它并将阻塞调用转换为非阻塞调用,停止虚拟线程并继续处理其他一些虚拟线程代码。

只有当没有虚拟线程准备好执行时,才会停止本地线程。

请注意,您的代码从不调用阻塞系统调用,它会调用 java 库(当前执行阻塞系统调用)。Project Loom 替换了您的代码和阻塞系统调用之间的层,因此可以做任何它想做的事情——只要您的调用代码的结果看起来相同。

于 2021-11-30T20:04:22.127 回答
1

Thomas Kläger的答案是正确的。我会补充一些想法。

据我了解,如果我的操作系统线程数量等于 CPU 内核数量和无限数量的虚拟线程,所有操作系统线程仍将等待 IO

不,不正确,你误会了。

您所描述的是在 Java 中当前线程技术下发生的情况。通过 Java 线程到主机 OS 线程的一对一映射,在 Java 中进行的任何阻塞(等待相对较长的响应时间)的调用都会使主机线程旋转它的拇指,不做任何工作。如果主机有数以万计的线程,这样就可以安排其他线程在 CPU 内核上工作,这将不是问题。但是主机操作系统线程非常昂贵,所以我们没有数以千计的,我们很少。

使用 Project Loom 技术,JVM 检测阻塞调用,例如等待 I/O。一旦检测到,JVM 在等待 I/O 响应时将虚拟线程搁置(“停放”)。JVM 将不同的虚拟线程分配给该主机操作系统载体线程,以便“真正的”线程可以继续执行工作,而不是在玩弄它的拇指时等待。由于 JVM 中的虚拟线程非常便宜(内存和 CPU 都非常高效),我们可以有数千甚至数百万个虚拟线程供 JVM 处理。

在您的示例中,每个线程等待 IO 响应形成对数据库的 JDBC 调用的 200 个线程,如果这些线程都是将全部停放在 JVM 中的虚拟线程。您用作载体线程的少数主机操作系统线程ExecutorService将在当前未阻塞的其他虚拟面包上工作。这种停放和重新调度阻塞然后解除阻塞的虚拟线程由 JVM 中的 Project Loom 技术自动处理,我们 Java 应用程序开发人员不需要干预。

假设我将线程池切换为使用虚拟线程

实际上,没有虚拟线程池。每个虚拟线程都是新鲜的,没有回收。这消除了对线程局部污染的担忧。

ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor() ;
…
executorService.submit( someTask ) ;  // Every task submitted gets assigned to a fresh new virtual thread.

要了解更多信息,我强烈建议您观看 Project Loom 团队成员 Ron Pressler 或 Alan Bateman 的演示和采访视频。查找最新的,因为 Loom 一直在发展。

并阅读新的 Java JEP,JEP 草案:Virtual Threads (Preview)

于 2021-11-30T20:21:35.477 回答
0

我终于找到了答案。所以正如我所说,默认情况下,InputStream.read方法会进行read()系统调用,根据 Linux 手册页将阻塞底层操作系统线程。那么Loom怎么可能不会阻止它呢?我找到了一篇显示堆栈跟踪的文章所以如果这个代码块将由虚拟线程执行

URLData getURL(URL url) throws IOException {
  try (InputStream in = url.openStream()) {//blocking call
    return new URLData(url, in.readAllBytes());
  }
}

JVM 运行时会将其转换为以下堆栈跟踪

java.base/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:60)//this line parks the virtual thread
java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:184)
java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:212)
java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:356)//JVM runtime will replace an actual read() into read from java nio package 
java.base/java.io.InputStream.readAllBytes(InputStream.java:346)

JVM 如何知道何时解除虚拟线程的停放?readAllBytes这是完成后将运行的堆栈跟踪

"Read-Poller" #16
  java.base@17-internal/sun.nio.ch.KQueue.poll(Native Method)
  java.base@17-internal/sun.nio.ch.KQueuePoller.poll(KQueuePoller.java:65)
  java.base@17-internal/sun.nio.ch.Poller.poll(Poller.java:195)

文章作者使用的是 MacOs,Mackqueue用作非阻塞 syscall,如果我在 Linux 上运行它,我会看到epollsyscall。

所以基本上 Loom 并没有引入任何新的东西,在底层它是一个epoll带有回调的普通系统调用,可以使用 Vert.x 等框架实现,它在底层使用 Netty,但在 Loom 中,回调逻辑是用 JVM 运行时封装的我发现反直觉,当我调用 InputStream.read() 时,我确实希望有一个相应的 read() 系统调用,但 JVM 会用非阻塞系统调用替换它。

于 2021-11-30T21:40:01.657 回答