虚拟线程简介:Java 并发的新方法

影响最深远的 Java 19 更新之一是虚拟线程的引入。 虚拟线程是 Project Loom 的一部分,在 Java 19 中作为预览提供。

虚拟线程如何工作

虚拟线程在操作系统进程和应用程序级并发之间引入了一个抽象层。 换句话说,虚拟线程可用于调度 Java 虚拟机编排的任务,因此 JVM 充当操作系统和程序之间的中介。 图 1 显示了虚拟线程的体系结构。

国际数据集团

图 1. Java 中虚拟线程的体系结构。

在此架构中,应用程序实例化虚拟线程,JVM 分配计算资源来处理它们。 将此与直接映射到操作系统 (OS) 进程的传统线程进行对比。 对于传统线程,应用程序代码负责供应和分配 OS 资源。 使用虚拟线程,应用程序实例化虚拟线程,从而表达对并发的需求。 但是从操作系统获取和释放资源的是JVM。

Java 中的虚拟线程类似于 Go 语言中的 goroutine。 使用虚拟线程时,JVM 只能在应用程序的虚拟线程处于停放状态时分配计算资源,这意味着它们处于空闲状态并等待新工作。 这种空闲在大多数服务器中很常见:它们为请求分配一个线程,然后它空闲,等待新事件,例如来自数据存储的响应或来自网络的进一步输入。

使用传统的 Java 线程,当服务器在请求时空闲时,操作系统线程也空闲,这严重限制了服务器的可伸缩性。 正如 Nicolai Parlog 所解释的那样,“操作系统无法提高平台线程的效率,但 JDK 将通过切断其线程与操作系统线程之间的一对一关系来更好地利用它们。”

以前为减轻与传统 Java 线程相关的性能和可伸缩性问题所做的努力包括异步、反应式库,如 JavaRX。 虚拟线程的不同之处在于它们是在 JVM 级别实现的,但它们适合 Java 中现有的编程结构。

使用 Java 虚拟线程:一个演示

对于这个演示,我使用 Maven 原型创建了一个简单的 Java 应用程序。 我还进行了一些更改以在 Java 19 预览版中启用虚拟线程。 一旦虚拟线程升级到预览之外,您就不需要进行这些更改。

清单 1 显示了我对 Maven 原型的 POM 文件所做的更改。 请注意,我还将编译器设置为使用 Java 19,并且(如清单 2 所示)向 .mvn/jvm.config.

清单 1. 演示应用程序的 pom.xml


<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <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>

--enable-preview 开关需要使 exec:java 在启用预览的情况下工作。 它使用所需的开关启动 Maven 进程。

清单 2. 将 enable-preview 添加到 .mvn/jvm.config


--enable-preview

现在,您可以使用 mvn compile exec:java 并且虚拟线程功能将编译和执行。

虚拟线程的两种使用方式

现在让我们考虑在代码中实际使用虚拟线程的两种主要方式。 虽然虚拟线程显着改变了 JVM 的工作方式,但其代码实际上与传统的 Java 线程非常相似。 这种相似性是设计使然,使得重构现有应用程序和服务器相对容易。 这种兼容性还意味着用于监视和观察 JVM 中线程的现有工具将与虚拟线程一起工作。

Thread.startVirtualThread(Runnable r)

使用虚拟线程的最基本方法是 Thread.startVirtualThread(Runnable r). 这是实例化线程和调用的替代 thread.start(). 查看清单 3 中的示例代码。

清单 3. 实例化一个新线程


package com.infoworld;

import java.util.Random;

public class App {
  public static void main( String[] args ) {
    boolean vThreads = args.length > 0;
    System.out.println( "Using vThreads: " + vThreads);

    long start = System.currentTimeMillis();

    Random random = new Random();
    Runnable runnable = () -> { double i = random.nextDouble(1000) % random.nextDouble(1000);  };  
    for (int i = 0; i < 50000; i++){
      if (vThreads){ 
        Thread.startVirtualThread(runnable);
      } else {
        Thread t = new Thread(runnable);
        t.start();
      }
    }
   
    long finish = System.currentTimeMillis();
    long timeElapsed = finish - start;
    System.out.println("Run time: " + timeElapsed);
  }
}

当带参数运行时,清单 3 中的代码将使用虚拟线程; 否则它将使用常规线程。 无论您选择哪种线程类型,该程序都会生成 50,000 次迭代。 然后,它用随机数做一些简单的数学运算,并跟踪执行需要多长时间。

要使用虚拟线程运行代码,请键入: mvn compile exec:java -Dexec.args="true". 要使用标准线程运行,请键入: mvn compile exec:java. 我做了一个快速的性能测试并得到了以下结果:

    使用虚拟线程:运行时:174 使用常规线程:运行时:5450

这些结果是不科学的,但运行时间的差异是巨大的。

还有其他使用方法 Thread 产生虚拟线程,比如 Thread.ofVirtual().start(runnable). 有关详细信息,请参阅 Java 线程文档。

使用执行器

启动虚拟线程的另一种主要方式是使用执行器。 执行器在处理线程方面很常见,它提供了一种标准方法来协调许多任务和线程池。

虚拟线程不需要池化,因为它们的创建和处理成本很低,因此池化是不必要的。 相反,您可以将 JVM 视为为您管理线程池。 然而,许多程序确实使用了执行器,因此 Java 19 在执行器中包含了一个新的预览方法,以便于重构到虚拟线程。 清单 4 向您展示了新方法和旧方法。

清单 4. 新的执行器方法


ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); // New method
ExecutorService executor = Executors.newFixedThreadPool(Integer poolSize); // Old method

此外,Java 19 引入了 Executors.newThreadPerTaskExecutor(ThreadFactory threadFactory) 方法,它可以采用 ThreadFactory 构建虚拟线程。 这样的工厂可以通过 Thread.ofVirtual().factory().

关于执行器方法的更多信息

有关执行程序方法的更多信息,请参阅执行程序文档。

虚拟线程的最佳实践

一般来说,因为虚拟线程实现了 Thread 类,它们可以在标准线程所在的任何地方使用。 但是,在如何使用虚拟线程以获得最佳效果方面存在差异。 一个示例是在访问数据存储等资源时使用信号量来控制线程数,而不是使用具有限制的线程池。 有关更多提示,请参阅 Coming to Java 19:虚拟线程和平台线程。

另一个重要的注意事项是虚拟线程始终是守护线程,这意味着它们将保持包含 JVM 进程的活动状态直到它们完成。 此外,您不能更改他们的优先级。 更改优先级和守护程序状态的方法是空操作。 有关更多信息,请参阅线程文档。

用虚拟线程重构

虚拟线程是引擎盖下的一个重大变化,但它们有意易于应用于现有代码库。 虚拟线程将对 Tomcat 和 GlassFish 等服务器产生最大和最直接的影响。 这样的服务器应该能够以最小的努力采用虚拟线程。 在这些服务器上运行的应用程序将在不更改代码的情况下获得可扩展性收益,这可能对大型应用程序产生巨大影响。 考虑在许多服务器和内核上运行的 Java 应用程序; 突然间,它将能够处理一个数量级的更多并发请求(当然,这完全取决于请求处理配置文件)。

像 Tomcat 这样的服务器允许带有配置参数的虚拟线程可能只是时间问题。 与此同时,如果您对将服务器迁移到虚拟线程感到好奇,请考虑 Cay Horstmann 的这篇博文,他在其中展示了为虚拟线程配置 Tomcat 的过程。 他启用虚拟线程预览功能并替换 Executor 使用仅一行不同的自定义实现(您猜对了, Executors.newThreadPerTaskExecutor). 可扩展性的好处是显着的,正如他所说:“有了这个改变,200 个请求需要 3 秒,而 Tomcat 可以轻松地接受 10,000 个请求。”

结论

虚拟线程是 JVM 的一个重大变化。 对于应用程序程序员,它们代表了异步样式编码的替代方法,例如使用回调或期货。 总而言之,在处理并发性时,我们可以将虚拟线程视为钟摆摆回 Java 中的同步编程范例。 这在编程风格上(尽管在实现上完全不同)大致类似于 JavaScript 引入的 async/await。 简而言之,用简单的同步语法编写正确的异步行为变得非常容易——至少在线程花费大量时间空闲的应用程序中是这样。

查看以下资源以了解有关虚拟线程的更多信息:

    虚拟线程:大规模 Java 应用程序的新基础 如何使用 Java 19 虚拟线程 Happy Coders 介绍 Java 中的虚拟线程Horstmann 关于将 Tomcat 迁移到虚拟线程的博文

阅读更多

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注