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

译文
开发
Java19影响最深远的更新之一是引入了虚拟线程。

作者 | Matthew Tyson

译者 | 李睿

  Java19影响最深远的更新之一是引入了虚拟线程。虚拟线程是Project Loom的一部分,可以在Java19预览版中使用。

虚拟线程如何工作

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

图片

图1.Java中虚拟线程的架构

  在这种架构中,应用程序实例化虚拟线程,并由JVM分配处理虚拟线程的计算资源。与此相比,常规线程直接映射到操作系统(OS)进程。对于常规线程,应用程序代码负责提供和分配操作系统资源。而使用虚拟线程,应用程序可以实例化虚拟线程,从而表达并发性的需求。但正是JVM从操作系统获取和释放资源。

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

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

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

使用Java虚拟线程:演示

  在这个演示中,创建了一个使用Maven原型的简单Java应用程序。为此还做了一些更改,以便在Java19预览版中启用虚拟线程。一旦虚拟线程被升级到预览之外,就不需要做这些更改了。

  清单1显示了对Maven原型的POM文件所做的更改。需要注意的是,还将编译器设置为使用Java19,并在.mvn/jvm.config中添加了一行(例如清单2所示)。

  清单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>

  要使exec:java在启用预览的情况下工作,必须使用enable-preview开关。它使用所需的开关启动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中的代码将使用一个虚拟线程,否则将使用常规线程。无论选择哪种线程类型,该程序都会生成5万次迭代。然后,它用随机数做一些简单的数学运算,并跟踪执行所需的时间。

  要使用虚拟线程运行代码,需要键入:mvn-compile-exec:java-Dexec.args=“true”。要使用标准线程运行,需要键入:mvn-compile-exec:java。为此进行了一个快速的性能测试,得到如下结果:

  • 带有虚拟线程:Runtime: 174
  • 使用常规线程:Runtime: 5450

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

  还有其他使用Thread生成虚拟线程的方法,例如Thread.ofVirtual().start(runnable)。

  使用执行器

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

  虚拟线程不需要使用线程池,因为创建和处理它们的成本很低,因此没有必要使用线程池。与其相反,可以将JVM看作是管理线程池。但是,许多程序确实使用执行器,因此Java19在执行器中包含了一个新的预览方法,使重构虚拟线程变得容易。清单4展示了新方法和旧方法。

  清单4.新的执行器方法

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

  左右滑动查看完整代码

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

虚拟线程的优秀实践

  一般来说,因为虚拟线程实现了线程类,所以它们可以在标准线程所在的任何地方使用。但是,在如何使用虚拟线程以获得最佳效果方面存在差异。一个例子是在访问数据存储等资源时使用信号量来控制线程数量,而不是使用有限制的线程池。

  另一个重要注意事项是,虚拟线程始终守护线程,这意味着它们将使包含它们的JVM进程保持活动状态,直到它们完成。此外,不能更改它们的优先级。更改优先级和守护进程状态的方法为无操作(no-ops)。

使用虚拟线程重构

  虚拟线程在本质上是一个很大的改变,但它们很容易应用到现有的代码库中。虚拟线程将对Tomcat和GlassFish等服务器产生最大、最直接的影响。这样的服务器应该能够以最小的努力采用虚拟线程。在这些服务器上运行的应用程序将获得可扩展性的收益,而无需对代码进行任何更改,这可能对大规模应用程序产生巨大影响。考虑一个运行在多个服务器和核心上的Java应用程序,突然之间它将能够处理一个数量级的并发请求,当然这完全取决于请求处理配置文件。

  像Tomcat这样的服务器允许带配置参数的虚拟线程可能只是时间问题。与此同时,如果对将服务器迁移到虚拟线程感到好奇,可以阅读Cay Horstmann撰写的一篇博客文章,他在文章中展示了为虚拟线程配置Tomcat的过程。他启用了虚拟线程预览功能,并将Executor替换为只差一行的自定义实现。可扩展性的好处是显著的,正如他在文章中所说:“通过这种更改,200个请求只需3秒,而Tomcat可以轻松处理10,000个请求。”

结论

  虚拟线程是JVM的一个主要变化。对于应用程序程序员来说,它们代表了异步风格编码(如使用回调)的另一种选择。总之,在处理Java并发性时,可以将虚拟线程看作是一个摆向Java中同步编程范式的钟摆。这在编程风格上大致类似于JavaScript引入的async/await(尽管在实现上完全不同)。简而言之,使用简单的同步语法编写正确的异步行为变得相当容易,至少在线程花费大量时间空闲的应用程序中是这样。

原文链接:

​https://www.infoworld.com/article/3678148/intro-to-virtual-threads-a-new-approach-to-java-concurrency.html​


责任编辑:张洁 来源: 51CTO技术栈
相关推荐

2022-04-20 08:00:00

深度学习数据集Hub

2022-03-10 12:16:14

侧信道内存攻击网络攻击

2021-02-18 18:13:34

LinuxARM树莓派

2016-12-26 18:39:32

Android应用进程存活率

2016-12-26 18:25:29

Android应用进程存活率

2018-10-07 07:00:59

2023-08-08 11:28:06

企业首席执行官

2010-06-18 09:48:22

2021-09-26 10:49:27

计算机互联网 技术

2024-11-05 08:19:11

深度学习神经网络机器学习

2015-08-21 09:14:40

大数据

2021-09-27 10:12:42

欺骗防御rMTD网络攻击

2022-05-26 10:57:51

机器人人工智能

2023-12-04 08:21:18

虚拟线程Tomcat

2022-07-07 10:47:16

IngressKubernetes

2019-07-12 13:50:36

物联网大数据安全

2014-05-20 16:27:35

JVMScala

2010-04-01 09:30:57

2024-10-23 19:47:54

2024-01-23 17:33:36

点赞
收藏

51CTO技术栈公众号