浅析Java中的虚拟线程

 更新时间:2023年10月27日 10:44:58   作者:翟睿  
在本篇文章中,小编将带大家深入了解Java虚拟线程的原理、如何使用、使用的注意事项以及其他相似技术的差别,感兴趣的小伙伴可以跟随小编一起学习一下

在最近发布的JDK 21 LTS版本中,加入了许多新特性。其中对我们开发人员影响最大的应该是分代ZGC以及Java的虚拟线程。在本篇文章中,我将带大家深入了解Java虚拟线程的原理、如何使用、使用的注意事项以及其他相似技术的差别。

什么是虚拟线程

首先,我们需要了解什么是虚拟线程。

在平时的开发过程中,我们所使用的多线程往往意味着平台线程。平台线程代表着JVM直接与操作系统交互,创建了一个一个的线程,并且在JVM中还要为这个线程单独开辟内存使用。一般在JVM中创建一个平台线程,开销大约在1M左右。为了避免创建线程和销毁线程带来的巨大开销,我们通常选择使用池化技术来维护一些活跃的线程。

此外,线程的数量也需要严格控制。如果在一个线程池中维护成千上百个线程,往往效率并不尽如人意。因为线程在切换时涉及到CPU上下文的切换,如果线程数过多,反而会降低执行效率。因此,如何控制线程池的大小也是考验工程师经验的难点。

平台线程与系统线程关系如下

为了解决这个问题,虚拟线程应运而生,虚拟线程并不是Java的首创,它在很多其他语言中被称为协程、纤程、绿色线程、用户态线程等,虚拟线程相对平台线程,并不直接与操作系统交互,虚拟线程的数据是维护在堆内存中,由JVM创建的平台线程来持有,由平台线程来决定什么时候来切换虚拟线程,大概图如下

虽然图中只画了几个虚拟线程,但是在实际使用中,我们可以创建成百上千的虚拟线程而不用担心资源消耗的问题

首先原因在于虚拟线程的开销极其廉价,一个虚拟线程可能才使用几百字节,所以几遍创建成百上千也不会消耗太多内存资源

其次虽然我们有极多的虚拟线程,但是实际上执行线程依旧只有几个平台线程,所以在线程使用中不会由于CPU上下文的切换导致的额外开销。

使用

接下来我们用代码来实践一下虚拟线程,JDK的工程师为了方便我们快速进行虚拟线程的升级,可以使用Thread来快速创建虚拟线程

Thread vt = Thread.startVirtualThread(() -> {
    Thread.sleep(1000);
});

这种方法创建的虚拟线程会立刻启动,那么如果我们想创建一个需要手动启动的虚拟线程,可以参考如下的方式

// 创建VirtualThread
Thread.ofVirtual().unstarted(() -> {
    Thread.sleep(1000);
});
// 运行
vt.start();

还可以创建虚拟线程的工厂来使用

// 创建ThreadFactory:
ThreadFactory tf = Thread.ofVirtual().factory();
// 创建VirtualThread:
Thread vt = tf.newThread(() -> {
    Thread.sleep(1000);
});
// 运行:
vt.start();

我们将线程的上下文信息打印出来看看

VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1

从这个打印内容可以大概看出,虚拟线程底层采用了ForkJoinPool来维护平台线程,而后面的worker-1则代表具体的平台线程的名称,前面的#21代表当前虚拟线程的数量,那么可能有同学就会问了 那如果我全局有成千上百个虚拟线程,我怎么知道是哪块业务的线程除了问题呢。

我们可以对线程工厂进行命名,使用该线程工厂命名后,在线程上下文中就会打印对应虚拟线程工厂的信息。

ThreadFactory factory = Thread.ofVirtual().name("myVirtual").factory();
        try(ExecutorService executor = Executors.newThreadPerTaskExecutor(factory)) {
            for (int i=0; i<100000; i++) {
                // 也可以直接传入Runnable或Callable:
                executor.submit(() -> {
                    System.out.println(Thread.currentThread());
                    Thread.sleep(1000);
                    return true;
                });
            }
        }

VirtualThread[#23,myVirtual]/runnable@ForkJoinPool-1-worker-2
VirtualThread[#275,myVirtual]/runnable@ForkJoinPool-1-worker-7
VirtualThread[#282,myVirtual]/runnable@ForkJoinPool-1-worker-5
VirtualThread[#285,myVirtual]/runnable@ForkJoinPool-1-worker-4
VirtualThread[#288,myVirtual]/runnable@ForkJoinPool-1-worker-3
VirtualThread[#234,myVirtual]/runnable@ForkJoinPool-1-worker-10

从日志中不难看出,我们已经成功在线程上下文标记了这一块虚拟线程属于哪个工厂,并且通过信息后缀中的worker-10可以看出底层的ForkJoinPool已经创建了10个平台线程来操作虚拟线程,因为我这台电脑是10核的,所以底层的线程池会创建与核心数相等的线程来操作虚拟线程,在我的实践中发现,无论你创建多少调度器,所有底层的虚拟线程都是由同一个平台线程池来操控的。

而且在示例代码中,我创建了十万个虚拟线程,如果我是用的是平台线程,且不说执行效率,但是内存分配就已经使项目OOM了。

注意事项

ThreadLocal支持

有同学可能会问,在虚拟线程中能否使用ThreadLocal对象呢?官方给出的答案是,能够使用但不建议。在过去,我们在维护线程池时,线程的数量是固定的且相对较少。我们通过手动清理的方式来重用ThreadLocal对象。然而,在使用虚拟线程后,我们不再关注具体的线程数,这导致ThreadLocal对象的数量无法控制,从而占用了额外的内存。需要注意的是,虚拟线程与平台线程不共享同一个ThreadLocal。

永远不要池化虚拟线程

之前我们提到过,虚拟线程的开销非常低,每个虚拟线程可能只消耗几百字节的内存。这意味着我们不需要为虚拟线程创建所谓的线程池。相应地,根据我们之前的编码习惯,如果某个功能只能接受20个并发请求,我们可能会创建一个固定大小为20的线程池来进行限流。然而,如果使用虚拟线程,我们应尽量改用类似信号量的方式来实现。

协程、IO多路复用与虚拟线程的关系

有很多同学可能对对这几个概念相对比较混淆,我们可以来梳理下这几个概念之间的异同,首先协程与虚拟线程从概念上是相同的,只是不同语言之间的实现方式是不同的,都是在用户态线程中维护多个子线程进行切换,只是如Python语言可能需要手动去唤起协程,而Java中的虚拟线程都是交由JVM调度的。

而IO多路复用,我们以Netty为例,它内部使用了IO多路复用技术来管理和处理大量的并发IO操作。Netty的IO多路复用机制可以有效地管理和调度多个连接,提高系统的吞吐量和性能。它基于事件驱动的设计模型,通过选择器(Selector)来同时监控多个IO通道的状态,当有IO事件就绪时,通过回调机制来进行处理。而虚拟线程通常用于解决IO阻塞的问题,通过在IO操作或时间等待点上主动释放执行权,来实现更好的并发性能。虚拟线程可以减少线程切换的开销,但它仍然是在应用程序内部执行的,并不能直接替代IO多路复用来管理和处理大量的并发IO操作。可以说术业有专攻,但是虚拟线程确实会对多IO操作有效率提升的。

到此这篇关于浅析Java中的虚拟线程的文章就介绍到这了,更多相关Java虚拟线程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java中深复制知识点详解

    java中深复制知识点详解

    在本篇文章里小编给大家整理了关于java中深复制知识点详解内容,有需要的朋友们可以学习下。
    2020-12-12
  • Java程序员必备的11大IntelliJ插件(附地址)

    Java程序员必备的11大IntelliJ插件(附地址)

    这篇文章主要介绍了Java程序员必备的11大IntelliJ插件(附地址),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • MySQL查询字段实现字符串分割split功能的示例代码

    MySQL查询字段实现字符串分割split功能的示例代码

    本文主要介绍了MySQL查询字段实现字符串分割split功能的示例代码,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • Java注解Annotaton详解

    Java注解Annotaton详解

    Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制,文中给大家介绍了三种基本的Annotaton,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2022-05-05
  • Java项目Guava包 HashMultimap使用及注意事项

    Java项目Guava包 HashMultimap使用及注意事项

    guava基本上可以说是java开发项目中,大概率会引入的包,今天介绍的主角是一个特殊的容器HashMultmap,可以简单的将它的数据结构理解为Map<K, Set<V>>,今天主要介绍下基础的知识点 HashMultmap级使用,感兴趣的朋友一起看看吧
    2022-05-05
  • java中BigDecimal里面的subtract函数介绍及实现方法

    java中BigDecimal里面的subtract函数介绍及实现方法

    在Java中实现减法操作需要根据数据类型选择不同方法,主要分为数值型减法和字符串减法两种场景,本文给大家介绍java中BigDecimal里面的subtract函数及实现方法,感兴趣的朋友一起看看吧
    2025-06-06
  • Java数据结构之常见排序算法(上)

    Java数据结构之常见排序算法(上)

    这篇文章主要介绍了Java数据结构之常见排序算法,本文章是汇总篇,且对每个排序都进行了说明,可以很好的理清思路,对排序算法有个总体的框架,需要的朋友可以参考下
    2023-01-01
  • nacos服务注册命名空间指定方式

    nacos服务注册命名空间指定方式

    文章介绍了Nacos服务注册命名空间的用途,以及如何创建和指定命名空间,命名空间用于隔离不同项目的服务和配置,避免冲突,通过在配置文件中指定命名空间ID,服务会注册到相应的命名空间中,这样可以更好地管理不同环境下的配置文件
    2024-12-12
  • 解决OOM:JVM内存不足问题

    解决OOM:JVM内存不足问题

    这篇文章主要介绍了解决OOM:JVM内存不足问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-06-06
  • 在idea中git pull失败的解决方案

    在idea中git pull失败的解决方案

    在遇到Git Pull失败时,首先使用IDEA的git-revert功能进行还原,然后检查并解决分支冲突,最后重新执行Git Pull确保所有文件是最新的,注意,在操作过程中确保网络连接正常,并且每步操作后都要执行Git Pull来更新数据
    2024-10-10

最新评论