作者:MatthewTyson|虚拟线程是ProjectLoom的一部分,可在Java19预览版中使用。虚拟线程如何工作 虚拟线程在操作系统进程和应用程序级并发之间引入了一个抽象层。换句话说,虚拟线程可以用于调度由Java虚拟机编排的任务,因此JVM充当操作系统和程序之间的中介。图1显示了虚拟线程的体系结构。图1.Java中虚拟线程的架构 在这种架构中,应用程序实例化虚拟线程,JVM分配计算资源来处理虚拟线程。相反,常规线程直接映射到操作系统(OS)进程。对于常规线程,应用程序代码负责提供和分配操作系统资源。使用虚拟线程,应用程序可以实例化虚拟线程来表达并发需求。但是从OS获取和释放资源的是JVM。 Java中的虚拟线程类似于Go语言中的goroutine。当使用虚拟线程时,JVM只能在应用程序的虚拟线程常驻时分配计算资源,这意味着它们处于空闲状态并等待新事件。这种空闲在大多数服务器中很常见:它们为请求分配一个线程,然后闲置并等待新事件,例如来自数据存储的响应或来自网络的进一步输入。 传统的Java线程,当服务器在处理请求时空闲,操作系统线程也空闲,严重限制了服务器的可扩展性。正如NicolaiParlog解释的那样,“操作系统无法提高平台线程的效率,但JDK可以通过切断其线程与操作系统线程之间的一对一关系来更好地利用它们。” PreviouslyMitigated解决与传统Java线程相关的性能和可伸缩性问题的努力包括异步、反应式库,例如JavaRX。虚拟线程的不同之处在于它们是在JVM级别实现的,但它们适用于Java中现有的编程结构。使用Java虚拟线程:演示 在此演示中,创建了一个使用Maven原型的简单Java应用程序。还进行了一些更改以在Java19预览版中启用虚拟线程。一旦虚拟线程升级为预览版,就不需要进行这些更改。 清单1显示了对Maven原型的POM文件所做的更改。请注意,编译器也设置为使用Java19,并且在.mvn/jvm.config中添加了一行(如清单2所示)。 清单1.演示应用程序的pom.xmlUTF-81919org.apache.maven.pluginsmaven-compiler-plugin<版本>3.10.1--add-modules=jdk.incubator.concurrent--enable-preview/配置> 要使exec:java启用预览,必须使用enable-preview开关。它使用所需的开关启动Maven进程。 清单2.将启用预览添加到.mvn/jvm.config--enable-preview 现在,可以使用mvncompileexec:java执行程序,虚拟线程功能将被编译和执行。使用虚拟线程的两种方法 现在考虑在代码中实际使用虚拟线程的两种主要方法。虽然虚拟线程对JVM的工作方式产生了巨大的影响,但它们的代码实际上与传统的Java线程非常相似。设计上的相似性使得重构现有应用程序和服务器相对容易。这种兼容性还意味着用于监视和观察JVM中线程的现有工具将与虚拟线程一起工作。 Thread.startVirtualThread(Runnabler) 使用虚拟线程最基本的方法是使用Thread.startVirtualThread(Runnabler))。这是实例化线程和调用thread.start()的替代方法。查看清单3中的示例代码。 清单3.实例化一个新的线程包com.infoworld;导入java.util.Random;publicclassApp{publicstaticvoidmain(String[]args){booleanvThreads=args.length>0;System.out.println("UsingvThreads:"+vThreads);longstart=System.currentTimeMillis();Randomrandom=newRandom();Runnablerunnable=()->{doublei=random.nextDouble(1000)%random.nextDouble(1000);};for(inti=0;i<50000;i++){if(vThreads){Thread.startVirtualThread(runnable);}else{Threadt=newThread(runnable);t.start();}}longfinish=System.currentTimeMillis();longtimeElapsed=finish-start;System.out.println("Runtime:"+timeElapsed);}} 清单3中的代码在带参数运行时虚拟线程将被使用,否则将使用常规线程。无论选择何种线程类型,程序都会生成50k次迭代。然后它用随机数做一些简单的数学运算,并跟踪执行所需的时间。 要使用虚拟线程运行代码,您需要键入:mvn-compile-exec:java-Dexec.args="true"。要使用标准线程运行,请键入:mvn-compile-exec:java。为此做了一个快速的性能测试,得到了以下结果:使用虚拟线程:运行时间:174使用常规线程:运行时间:5450 这些结果是不科学的,但运行时间的差异是巨大的。 还有其他方法可以使用Thread生成虚拟线程,比如Thread.ofVirtual().start(runnable)。 使用Executors 另一种启动虚拟线程的主要方式是使用Executors。执行器在处理线程时很常见,它提供了一种协调许多任务和线程池的标准方法。 虚拟线程不需要使用线程池,因为它们的创建和处理成本很低,所以不需要使用线程池。相反,JVM可以被认为是管理一个线程池。然而,许多程序确实使用了执行器,因此Java19在执行器中包含了一个新的预览方法,可以很容易地重构虚拟线程。清单4显示了新方法和旧方法。 清单4.新的执行器方法ExecutorServiceexecutor=Executors.newVirtualThreadPerTaskExecutor();//新方法ExecutorServiceexecutor=Executors.newFixedThreadPool(IntegerpoolSize);//老方法 左右滑动查看完整代码 另外,Java19引入了Executors.newThreadPerTaskExecutor(ThreadFactorythreadFactory)方法,可以使用构建虚拟线程的ThreadFactory。这样的线程工厂可以通过Thread.ofVirtual().factory()获得。虚拟线程的良好实践 一般来说,由于虚拟线程实现了Thread类,因此它们可以在任何存在标准线程的地方使用。但是,在如何使用虚拟线程以达到最佳效果方面存在差异。一个例子是在访问数据存储等资源时使用信号量来控制线程数,而不是使用有限的线程池。 另一个重要的注意事项是虚拟线程始终是守护线程,这意味着它们将使包含它们的JVM进程保持活动状态直到它们完成。此外,它们的优先级不能更改。更改优先级和守护程序状态的方法是空操作。重构 虚拟线程以使用虚拟线程本质上是一个很大的变化,但它们很容易应用于现有的代码库。虚拟线程将对Tomcat和GlassFish等服务器产生最大和最直接的影响。这样的服务器应该能够以最小的努力使用虚拟线程。在这些服务器上运行的应用程序将获得可扩展性,而无需对代码进行任何更改,这会对大型应用程序产生巨大影响。考虑一个在多个服务器和内核上运行的Java应用程序,突然间它将能够处理一个数量级的并发请求,当然这完全取决于请求处理配置文件。 像Tomcat这样的服务器允许带有配置参数的虚拟线程可能只是时间问题。同时,如果您对将服务器迁移到虚拟线程感到好奇,可以阅读CayHorstmann的博客文章,他在其中展示了为虚拟线程配置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