本文来自InfoQ中文网,原作者DeepuKSasidharan。推荐语:我们开发的网站,如果访问量过大,请求激增,需要考虑相关的并发问题。异步并发意味着适应更复杂的编程风格。Java中的传统线程非常重,与操作系统线程是一对一绑定的。Loom是jave生态系统中较新的项目,它试图解决传统并发模型的局限性。但是如何实现它在这篇文章中有详细的解释。随着ProjectLoom的加入,或许在未来,java生态的性能会发生一个数量级的提升。——MobTech高级java开发工程师Java19日前发布,最引人注目的特性是虚拟线程。本文介绍Loom项目中虚拟线程和结构化编程的基础知识。并将其与操作系统线程进行了比较。Java在发展初期就有很好的多线程和并发能力,可以高效利用多线程和多核CPU。JavaDevelopmentKit(JDK)1.1为平台线程(或操作系统(OS)线程)提供了基本支持,而JDK1.5提供了更多实用程序和更新以改进并发性和多线程。JDK8带来了异步编程支持和更多的并发改进。尽管在许多不同的版本中得到了改进,但Java在过去的三十多年里除了基于操作系统的并发和多线程支持之外没有任何突破。Java中的并发模型虽然非常强大和灵活,但并不是最容易使用的,开发者体验也不是很好。这主要是因为它默认使用的共享状态并发模型。我们必须使用同步线程来避免数据竞争和线程阻塞等问题。我曾经在一篇名为“现代编程语言中的并发性:Java”的博文中讨论过Java并发性问题。什么是Loom项目?ProjectLoom旨在大幅减少与编写、维护和观察以最佳方式利用现有硬件的高吞吐量并发应用程序相关的工作量。-RonPressler(Loom项目技术主管)操作系统线程是Java并发模型的核心,围绕它们有一个非常成熟的生态系统,但它们也有一些缺点,例如计算成本高。让我们看看两个最常见的并发用例,以及当前Java并发模型在这些用例中的缺点。并发最常见的用例之一是在服务器的帮助下通过网络处理请求。在这种情况下,首选方法是“每个请求一个线程”模型,其中每个请求都由一个单独的线程处理。这样一个系统的吞吐量可以用Little定律来计算,它指出在一个稳定的系统中,平均并发度(服务器并发处理的请求数)L等于吞吐量(平均请求率)λ的乘积通过延迟(处理每个请求的平均时间)W。基于此,我们可以得出结论,吞吐量等于平均并发数除以延迟(λ=L/W)。因此,在“每个请求一个线程”模型中,吞吐量将受到操作系统线程数的限制,这取决于硬件上可用的物理内核/线程数。要解决这个问题,我们不得不使用共享线程池或者异步并发,这两者都有各自的缺点。线程池有很多限制,例如线程泄漏、死锁、资源激增等。异步并发意味着必须适应更复杂的编程风格并谨慎处理数据竞争。它们还有可能出现内存泄漏、线程锁等问题。另一个常见的使用场景是并行处理或多线程,我们可能会将一个任务拆分为跨越多个线程的子任务。此时,我们必须编写避免数据损坏和数据竞争的解决方案。在某些情况下,在执行分布在多个线程上的并行任务时,还需要保证线程同步。这样的实现将非常脆弱,并且让开发人员承担很多责任,以确保不存在线程泄漏和取消延迟等问题。Loom项目旨在通过引入虚拟线程和结构化并发这两个新特性来解决当前并发模型中的这些问题。虚拟线程Java19已于2022年9月20日发布,虚拟线程是预览功能之一。虚拟线程是轻量级线程,不绑定操作系统线程,而是由JVM管理。它们适用于“每个请求一个线程”的编程风格,不受操作系统线程的限制。我们能够在不影响吞吐量的情况下创建数百万个虚拟线程。这与Go编程语言(Golang)协程(如goroutines)非常相似。Java19中新的虚拟线程特性易于使用。这里我将其与Golang的goroutines和Kotlin的coroutines进行对比。虚拟线程Thread.startVirtualThread(()->{System.out.println("Hello,ProjectLoom!");});Goroutinegofunc(){println("Hello,Goroutines!")}()KotlincoroutinerunBlocking{launch{println("Hello,Kotlincoroutines!")}}冷知识:在JDK1.1之前,Java用于支持绿色线程(也称为作为虚拟线程),但是这个特性在JDK1.1中被移除了,因为实现和平台线程没什么两样。在JVM中完成了一种新的虚拟线程实现,它将多个虚拟线程映射到一个或多个操作系统线程,开发者可以根据需要使用虚拟线程或平台线程。这个虚拟线程的实现还有以下几点考虑:在代码、运行时、调试器和探查器(profiler)中,它是一个Thread。它是一个Java实体,而不是本地线程的包装器。创建和阻止它们是廉价的操作。它们不应放置在游泳池中。虚拟线程使用基于工作窃取的ForkJoinPool调度程序。可插拔调度器可用于异步编程。虚拟线程将拥有自己的堆栈内存。虚拟线程的API与平台线程非常相似,因此更易于使用或移植。让我们看几个演示虚拟线程强大功能的示例。线程总数首先,让我们看看一台机器上可以创建多少个平台线程和虚拟线程。我的机器是IntelCorei9-11900H处理器,8核16线程,64GB内存,操作系统是Fedora36。平台线程varcounter=newAtomicInteger();while(true){newThread(()->{intcount=counter.incrementAndGet();System.out.println("Threadcount="+count);LockSupport.park();}).start();在我的机器上,代码在创建32,539个平台线程后崩溃。虚拟线程varcounter=newAtomicInteger();while(true){Thread.startVirtualThread(()->{intcount=counter.incrementAndGet();System.out.println("Threadcount="+count);LockSupport.park();});}在我的机器,进程在创建14,625,956个虚拟线程后挂起,但它没有崩溃,它只是随着内存逐渐可用而继续缓慢运行。您可能想知道为什么会这样。这是因为停放的虚拟线程被垃圾收集,JVM可以创建更多的虚拟线程并将它们分配给底层平台线程。任务吞吐量我们尝试使用平台线程运行100,000个任务。try(varexecutor=Executors.newThreadPerTaskExecutor(Executors.defaultThreadFactory())){IntStream.range(0,100_000).forEach(i->executor.submit(()->{Thread.sleep(Duration.ofSeconds(1));System.out.println(i);returni;}));}这里我们使用了带有默认线程工厂的newThreadPerTaskExecutor方法,因此使用了一个线程组。运行这段代码并计时,我得到以下结果。使用Executors.newCachedThreadPool()线程池时我获得了更好的性能。#'newThreadPerTaskExecutor'with'defaultThreadFactory'0:18.77real,18.15suser,7.19ssys,135%3891pu,0amem,743584mmem#'newCachedThreadPool'with'defaultThreadFactory'0:11.52real,13.24ssys,s13.24,157%6019pu,0amem,2215972mmem看起来不错。现在,让我们用虚拟线程完成同样的任务。try(varexecutor=Executors.newVirtualThreadPerTaskExecutor()){IntStream.range(0,100_000).forEach(i->executor.submit(()->{Thread.sleep(Duration.ofSeconds(1));System.out.println(i);returni;}));}运行这段代码并计时,我得到以下结果:0:02.62real,6.83suser,1.46ssys,316%14840pu,0amem,350268mmem这比基于平台线程的线程池好太多了。当然,这些都是很简单的使用场景,线程池和虚拟线程的实现还可以进一步优化以获得更好的性能,但这不是本文的重点。使用相同的代码运行JavaMicrobenchmarkHarness(JMH),结果如下。可以看出,虚拟线程的性能要比平台线程好很多。#ThroughputBenchmarkModeCntScoreErrorUnitsLoomBenchmark.platformThreadPerTaskthrpt50.362±0.079ops/sLoomBenchmark.platformThreadPoolthrpt50.528±0.067ops/sLoomBenchmark.virtualThreadPerTaskthrpt51.843±0.093ops/s#AveragetimeBenchmarkModeCntScoreErrorUnitsLoomBenchmark.platformThreadPerTaskavgt55.600±0.768s/opLoomBenchmark.platformThreadPoolavgt53.887±0.717s/opLoomBenchmark.virtualThreadPerTaskavgt51.098±0.020s/op您可以在GitHub上找到此基准测试的源代码。以下是其他几个有价值的虚拟线程基准测试:ElliotBarlas在GitHub上使用ApacheBench做了一个有趣的基准测试。AlexanderZakusylo在Medium上使用Akkaactors进行基准测试。GitHub上ColinCachia的I/O和非I/O任务的JMH基准测试。结构化并发结构化并发是Java19中的一个孵化特性。结构化并发的目的是简化多线程和并行编程。它将在不同线程中运行的多个任务视为一个工作单元,简化了错误处理和任务取消,同时提高了可靠性和可观察性。这有助于避免线程泄漏和取消延迟等问题。作为孵化特征,它可能会在稳定过程中发生进一步的变化。让我们考虑以下使用java.util.concurrent.ExecutorService的示例。voidhandleOrder()throwsExecutionException,InterruptedException{voidhandleOrder()throwsExecutionException,InterruptedException{try(varesvc=newScheduledThreadPoolExecutor(8)){Future
