一文详解JDK21中虚拟线程

 更新时间:2023年10月03日 10:03:47   作者:离离原上草77  
虚拟线程是JDK19中引入的,JDK21正式发布,本文主要介绍了JDK21中虚拟线程,具有一定的参考价值,感兴趣的可以了解一下

JEP 425: Virtual Threads (Preview) 虚拟线程,轻量级的线程模型对标其他语言中的协程,能够显著的减少编写、维护和观察高并发应用程序的工作量。

该特性的目标:

  • 支持服务端应用程序以thread-per-request样式编写,并最大限度压榨硬件性能。
  • 兼容java.lang.Thread API,减少调用方代码改动。
  • 兼容现有的JDK工具,用于故障排查、调整和分析。
  • 不会替代线程的传统实现,也不会静默升级现有的线程模型
  • 不会改变Java的并发编程模型
  • 在Java语言或Java库中提供新的数据并行结构,不会动摇Stream API的地位。

创建虚拟线程

虚拟线程并没有引入独立的API,而是选择兼容Thread的API。这样已有应用程序只需要做最小的改动就能使用协程带来的好处。

Thread thread = Thread.ofVirtual().name("duke").unstarted(runnable);
Thread.startVirtualThread(Runnable)
Thread.isVirtual()

为了支持虚拟线程,Thread API被稍微修改,ThreadGroup不再支持

虚拟线程vs平台线程

虚拟线程适合大量任务数,低CPU负载的任务。如果是CPU为瓶颈的应用,使用协程不会有性能改进。

虚拟线程的调度

Java虚拟线程采用M:N模式,即M个协程在N个线程中运行。调度线程默认和核心数一样多。可以通过 system property jdk.virtualThreadScheduler.parallelism来配置。

虚拟线程的载体线程不是固定的,在生命其中可能运行在不同的载体线程中。载体(carrier)线程和虚拟线程的堆栈信息是分离的,不会相互影响。

ThreadLocal和currentThread获取的都是独立的。在java代码曾来说虚拟线程后面的系统线程是不可见的。

虚拟线程的执行

虚拟线程会在IO阻塞或者其他等待的时候挂起,在可用的时候恢复执行。这不会阻塞载体线程,载体线程会去运行新任务。受限于系统,个别的阻塞操作可能也会阻塞载体线程,这时候可能会临时扩大调度池的大小。

一些情况下会钉住(pinned)载体线程,当同步方法/块或者当执行本地/外部函数时。被钉住的虚拟线程在执行很多操作的时候会阻塞背后的系统线程。高频或者长时间的synchronized使用java.util.concurrent.locks.ReentrantLock来代替,可以减少钉住。

JDK Flight Recorder可以查看被钉住的线程。-Djdk.tracePinnedThreads=full参数将能获取到完整的钉住线程的堆栈信息。将来可能会改进pinning inside synchronized的情况。

内存使用和垃圾收集器

虚拟线程的堆栈分配在垃圾收集的堆上。栈在运行时的增长和缩减,所以占用的内存少,非常的高效。

虚拟线程不是垃圾收集的根,不再使用的虚拟线程将会被回收掉。

由于虚拟线程通常比较多,Thread-local开销就会比较大,需要谨慎使用。

已有应用迁移到虚拟线程

只需要三步:

1.将普通线程的创建改成创建虚拟线程。

2.取消池化机制。因为虚拟线程非常轻量级,不需要池化。

3.synchronized改为ReentrantLock,以减少被钉住。被钉住的虚拟线程容易阻塞背后的载体线程。

和async/await的比较

async/await比有栈协程在某些情况下更好用。例如请求c需要a和b作为参数的情况下,async/await比有栈协程实现起来更加简单直接:

a = requestA()
b = requestB()
c = await request(await a, await b)

写一个简单的例子,开n个虚拟线程,每个线程sleep 1秒后累加1次,以模拟IO密集型操作。

    private static void runVirtualThread(int length) {
        AtomicInteger ai = new AtomicInteger();
        long start = System.currentTimeMillis();
        try (ExecutorService es = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < length; i++) {
                es.submit(() -> {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (ai.incrementAndGet() >= length) {
                        System.out.printf("duration=%d ms, done.%n", System.currentTimeMillis() - start);
                    }
                });
            }
        }
    }

然后再使用ScheduledExecutorService配合JMXBean打印JVM线程状态

        ScheduledExecutorService se = Executors.newScheduledThreadPool(1);
        se.scheduleAtFixedRate(() -> {
            ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
            ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);
            System.out.printf("threadNumber=%d%n", threadInfo.length);
        }, 100, 1000, TimeUnit.MILLISECONDS);
        runVirtualThread(100000);
        // runThread(100000);
        Thread.sleep(100 * 1000);
        se.shutdownNow();

再写一个使用传统操作系统线程的对比程序

    private static void runThread(int length) {
        AtomicInteger ai = new AtomicInteger();
        long start = System.currentTimeMillis();
        try (ExecutorService es = Executors.newCachedThreadPool()) {
            for (int i = 0; i < length; i++) {
                es.submit(() -> {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (ai.incrementAndGet() >= length) {
                        System.out.printf("duration=%d ms, done.%n", System.currentTimeMillis() - start);
                    }
                });
            }
        }
    }

执行

分别执行虚拟线程和操作系统线程,入参length为100,000,执行环境为Win11,Intel i5-12600KF。

  • 使用虚拟线程时输出如下:

 threadNumber=26
threadNumber=26
threadNumber=26
threadNumber=26
duration=3530 ms, done.

  • 操作系统线程时输出如下:

 threadNumber=2613
threadNumber=11072
threadNumber=15925
threadNumber=19725
threadNumber=22336
threadNumber=23933
duration=11614 ms, done.

结论

分别执行10次并删除坏点后,执行结果如下:

线程类型执行时间(ms)JVM线程数量打开句柄数量
虚拟线程3,40026600
操作系统线程11,50023000124,000

如上表所示,虚拟线程在执行IO密集型高并发任务时,性能远优于操作系统线程方式,对于操作系统资源的占用也降低很多.

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

相关文章

  • Java实战员工绩效管理系统的实现流程

    Java实战员工绩效管理系统的实现流程

    只学书上的理论是远远不够的,只有在实战中才能获得能力的提升,本篇文章手把手带你用java+SSM+Mysql+Maven+HTML实现一个员工绩效管理系统,大家可以在过程中查缺补漏,提升水平
    2022-01-01
  • IntelliJ IDEA 2021.1 推出语音、视频功能,边写代码边聊天(功能超级强大)

    IntelliJ IDEA 2021.1 推出语音、视频功能,边写代码边聊天(功能超级强大

    这篇文章主要介绍了IntelliJ IDEA 2021.1 推出语音、视频功能,边写代码边聊天(功能超级强大),本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-04-04
  • 浅谈Servlet开发技术基础

    浅谈Servlet开发技术基础

    这篇文章主要介绍了浅谈Servlet开发技术基础,具有一定借鉴价值,需要的朋友可以参考下。
    2017-12-12
  • Java 利用dom方式读取、创建xml详解及实例代码

    Java 利用dom方式读取、创建xml详解及实例代码

    这篇文章主要介绍了Java 利用dom方式读取、创建xml的相关资料,需要的朋友可以参考下
    2017-03-03
  • 总结Java常用的时间相关转化

    总结Java常用的时间相关转化

    今天给大家带来的是关于Java的相关知识,文章围绕着Java常用的时间相关转化展开,文中有非常详细的介绍及代码示例,需要的朋友可以参考下
    2021-06-06
  • Spring中@Scheduled功能的使用方法详解

    Spring中@Scheduled功能的使用方法详解

    @Scheduled 由Spring定义,用于将方法设置为调度任务,下面这篇文章主要给大家介绍了关于Spring中@Scheduled功能的使用方法,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2022-04-04
  • Spring @Bean 修饰方法时注入参数的操作方法

    Spring @Bean 修饰方法时注入参数的操作方法

    对于 Spring 而言,IOC 容器中的 Bean 对象的创建和使用是一大重点,Spring 也为我们提供了注解方式创建 bean 对象:使用 @Bean,这篇文章主要介绍了Spring @Bean 修饰方法时如何注入参数,需要的朋友可以参考下
    2023-10-10
  • 零基础入门SpringMVC拦截器的配置与使用

    零基础入门SpringMVC拦截器的配置与使用

    Spring MVC 的拦截器(Interceptor)与 Java Servlet 的过滤器(Filter)类似,它主要用于拦截用户的请求并做相应的处理,通常应用在权限验证、记录请求信息的日志、判断用户是否登录等功能上。本文将代码演示和文字描述详解拦截器的原理与使用
    2022-04-04
  • java版微信和支付宝退款接口

    java版微信和支付宝退款接口

    这篇文章主要为大家详细介绍了java版微信退款接口和java版支付宝退款接口,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-09-09
  • 在java程序中使用protobuf

    在java程序中使用protobuf

    这篇文章主要介绍了protobuf的基本使用和同java结合的具体案例,感性兴趣的小伙伴可以一起来阅读下文
    2021-08-08

最新评论