草庐IT

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

李睿 2023-03-29 原文

作者 | 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​


有关虚拟线程简介:Java并发性的一种新方法的更多相关文章

  1. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  2. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  3. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  4. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

  5. ruby - 为什么在 ruby​​ 中创建 Rational 不需要新方法 - 2

    这个问题在这里已经有了答案:关闭10年前。PossibleDuplicate:Rubysyntaxquestion:Rational(a,b)andRational.new!(a,b)我正在阅读ruby镐书,我对创建有理数的语法感到困惑。Rational(3,4)*Rational(1,2)产生=>3/8为什么Rational不需要new方法(我还注意到例如我可以在没有new方法的情况下创建字符串)?

  6. java - 什么相当于 ruby​​ 的 rack 或 python 的 Java wsgi? - 2

    什么是ruby​​的rack或python的Java的wsgi?还有一个路由库。 最佳答案 来自Python标准PEP333:Bycontrast,althoughJavahasjustasmanywebapplicationframeworksavailable,Java's"servlet"APImakesitpossibleforapplicationswrittenwithanyJavawebapplicationframeworktoruninanywebserverthatsupportstheservletAPI.ht

  7. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  8. 【Java 面试合集】HashMap中为什么引入红黑树,而不是AVL树呢 - 2

    HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候

  9. 在VMware16虚拟机安装Ubuntu详细教程 - 2

    在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主

  10. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

随机推荐