14小时前  Java系列 |   抢沙发  2 
文章评分 0 次,平均分 0.0

虚拟线程和平台线程性能对比

探索虚拟线程的世界真是令人兴奋,这是Java中的一项强大功能,有望彻底改变多线程应用程序的面貌。在本文中,我们将深入探讨虚拟线程如何提升应用程序的性能和可扩展性,同时将线程管理的开销降至最低。让我们踏上这段旅程,充分挖掘虚拟线程的潜力吧!

为了验证这一用例,我们将创建一百万个平台线程和虚拟线程。生成这些线程后,我们将使用HeapHero和fastThread工具分析它们的堆和线程行为。通过这一探索,我们旨在凸显平台线程和虚拟线程在性能上的差异。

Java中的虚拟线程(VT)是什么?

虚拟线程是一种创建不与特定操作系统线程绑定的线程的方式。这对于需要创建大量线程的应用程序非常有用,因为它可以减少创建和管理每个线程的开销。此外,对于需要创建非临时线程的应用程序,虚拟线程也很有用,因为它们可以保证每个线程都有机会运行。

此功能是在Java 21中引入的。虚拟线程也被称为“绿色线程”或“轻量级线程”。它是线程的一种软件实现,利用操作系统的线程来实现并发。它们由Java虚拟机(JVM)管理,对程序员不可见。

虚拟线程为何如此特别?

虚拟线程是一种特殊类型的线程,由平台线程创建,创建时仅占用极少的资源。得益于这一特性,可以生成大量虚拟线程,用于多线程编程。

由于创建虚拟线程的成本非常低,因此它不会像平台线程那样抛出任何错误。虚拟线程的另一个优点是,不需要像Java中的平台线程那样对它们进行池化。

平台线程

基本上,平台线程是JDK中的原生线程(java.lang.Thread)。

我们打算使用Java中的Thread类生成一百万个线程。在创建如此多的线程的过程中,操作系统将变得非常不稳定,并抛出OutOfMemoryError错误。我们将在Ubuntu Linux操作系统中实验这一行为。本实验使用的JDK版本为21。

static int cnt = 1;
 public static void main(String[] args) {
for(int i = 0; i<1000000; i++) {
         new Thread(
                 new Runnable() {
                     @Override
                     public void run() {
                         try {
                             TimeUnit.HOURS.sleep(1);
                         } catch (Exception ex) {}
                     }
                 }
         ).start();
       cnt++;
     }
 }

在上述代码中,我们在一个for循环中创建了一百万个线程,并让每个线程休眠长达1小时。当一个线程处于休眠状态时,所有资源都会被操作系统缓存。在这种特殊情况下,操作系统需要将每个线程的所有资源保持更长的时间,这是一项非常耗费资源的操作。请记住,在Java中创建线程是一项非常昂贵的操作。这就是在多线程编程范式中,需要在应用程序启动期间对线程进行池化的原因。

很快,上述代码将抛出OutOfMemory错误。您可以在下图中看到该错误:

虚拟线程和平台线程性能对比

虚拟线程

现在,让我们为虚拟线程开发一段代码。用例相同,但我们将在一个循环中动态生成一百万个虚拟线程。

static int cnt = 1;
public static void main(String[] args) {
   var executor = Executors.newVirtualThreadPerTaskExecutor();
   IntStream.range(1, 1000000).forEach(i ->{
       Future result =  executor.submit(() ->{
           try {
               TimeUnit.HOURS.sleep(1);
           } catch (InterruptedException e) {
               throw new RuntimeException(e);
           }
           String uuid = UUID.randomUUID().toString();
       });
       if(cnt == 999999) {
           generateHeapDump(2);
       }
       cnt++;
   });
}

在上述代码中,通过调用java.util.concurrent包中的ExecutorService类的newVirtualThreadPerTaskExecutor()方法,动态创建了一百万个虚拟线程。

在代码执行过程中,我们将使用generateHeapDump()方法对堆内存进行快照。基本上,当计数器达到999999时,我们会进行堆内存转储。这样,我们就能确保堆内存日志中捕获到最多的数据。

内存溢出错误

什么是OutOfMemoryError?当应用程序处理事务时,系统内存不足时,就会抛出此错误。那么,为什么我们在平台线程中会遇到OutOfMemoryError,而在虚拟线程中却不会呢?

在下图的帮助下,我们将对此有更多了解:

虚拟线程和平台线程性能对比

内存分为三个部分——堆、元空间和其他。对于平台线程,线程堆栈存储在“其他”区域。

每个线程都有独立的内存,这些内存存储在“其他”区域。当为线程分配内存且进程结束后,应释放这些内存。在平台线程场景中,线程会等待一小时,而与它们相关的内存并不会迅速释放。此外,还会再次生成新线程,它们也会等待一小时。这样一来,需要在“其他”区域分配大量内存,而JVM无法快速释放这些内存。因此,会抛出此错误。这段视频可以在10分钟内解释清楚JVM。

在虚拟线程的情况下,虚拟线程(VTs)被存储在由JVM进程控制的“堆”区域中,因为它们被视为对象。当你运行虚拟线程场景的代码时,你可以看到它会创建一百万个虚拟线程并休眠一小时。这些虚拟线程保存在堆内存中,生成线程所需的资源非常少。因此,在这种情况下,它不会抛出OutOfMemoryError错误。

注意:有时,在虚拟线程的情况下,它也可能抛出OutOfMemoryError异常。这是因为当创建大量虚拟线程时,“堆内存”将被耗尽。但在上述情况下,它不会抛出OOM错误,因为默认内存足以容纳一百万个虚拟线程!

我们将通过分析平台线程和虚拟线程的线程与内存行为来验证上述理论。

平台线程性能对比

我们将使用fastThread和HeapHero工具集,分别进行线程和堆转储分析,从而对平台线程性能进行对比研究。

线程转储分析

这是由fastthread.io为平台线程生成的线程转储报告。该报告足够智能,能够提供发生OutOfMemoryError(内存溢出错误)的可能性。

虚拟线程和平台线程性能对比

报告指出,JVM中有近1600个线程,这些数字令人担忧。本报告的第一部分为我们提供了关于应用程序状态的足够信息。

现在,让我们快速检查具有相同堆栈跟踪的线程。以下是相关图表。

虚拟线程和平台线程性能对比

此图展示了具有相同堆栈跟踪的多个线程。这些线程处于等待阶段。这是因为应用程序创建了大量线程,并要求这些线程等待一小时(有关平台线程代码,请参考Thread.sleep(..))。大约有1600个线程被要求等待。因此,报告会显示具有相同行为的堆栈跟踪。

然而,在报告的剩余部分中值得注意。这是详细报告的链接,您可以查看并更好地理解。

堆转储分析

以下是使用heaphero.io对同一平台线程进行的堆转储分析。

虚拟线程和平台线程性能对比

从这里可以看出,堆大小非常小。因此我们可以说,在平台线程的情况下,这些数字非常高。这很可能会给应用程序带来问题。以下是使用HeapHero工具集对平台线程进行故障排除的报告。

虚拟线程性能对比

我们将分别使用fastThread和HeapHero工具集,通过执行线程和堆转储分析,对虚拟线程的性能进行对比研究。

线程转储分析

以下是虚拟线程的线程转储分析。您可以看到线程数量约为37个。为什么会这样?为什么报告中没有显示这100万个线程?

虚拟线程和平台线程性能对比

这是因为虚拟线程不被视为真正的线程,所以在获取线程转储时,它们不会被包含在报告中。这份线程转储智能报告会告诉你,与此同时,堆大小可能会增长。

堆转储分析

现在,让我们使用HeapHero网站来分析虚拟线程的报告。生成的报告可能有些庞大,您需要等待一段时间才能获得详细报告。

虚拟线程和平台线程性能对比

首先,看一下这份报告,并花些时间研究一下。这份报告指出,存在999999个java.lang.VirtualThreads实例。所有这些线程都是从一个jdk.internal.misc.CarrierThread实例引用的。

虚拟线程和平台线程性能对比

这份报告的有趣之处在于,堆的大小为401 MB。当执行与虚拟线程相关的代码时,JVM会将这100万个虚拟线程的所有信息都保存到堆区域。因此,在这种情况下,堆的大小非常大。这就是关键所在。这些数据当然也符合垃圾收集的条件。以下是堆分析报告,其中对此进行了重点说明。

平台与虚拟线程性能对比

现在,让我们根据下表对比线程数量与堆大小:

Thread Count Heap Size
Platform Threads Test 1599(后续发生OOM) 1.85M
Virtual Threads Test 1,000,000(正常) 401M

当平台线程的代码运行时,它会生成近1600个线程,然后抛出OutOfMemoryError异常。但在这种情况下,堆的大小相对较小。这是因为,如本文前文所述,线程堆栈保存在其他区域中,而不是堆中。

在虚拟线程的情况下,应用程序创建的线程数量相对较少,但堆的大小非常大。这是因为虚拟线程使用的是堆内存。

结论

虚拟线程是创建多线程应用程序的有用工具。它们可以通过使用多个线程并行执行任务来提高应用程序的性能。虚拟线程的使用方式与多线程应用程序中平台线程的使用方式相同。创建和管理每个线程没有开销,但仍然能产生更好的结果。这显然是Java语言中的一项强大功能,并且随着这一特性的加入,应用程序的扩展变得非常容易。这是使用虚拟线程的明显优势。

原文地址: https://blog.fastthread.io/virtual-threads-a-definite-advantage/

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/3027.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册