• JDK21# 虚拟线程vs平台线程


    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()
    
    • 1
    • 2
    • 3

    为了支持虚拟线程,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)
    
    • 1
    • 2
    • 3

    写一个简单的例子,开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);
                        }
                    });
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    然后再使用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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

        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);
                        }
                    });
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    执行

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

    1. 使用虚拟线程时输出如下:
    threadNumber=26
    threadNumber=26
    threadNumber=26
    threadNumber=26
    duration=3530 ms, done.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 操作系统线程时输出如下:
    threadNumber=2613
    threadNumber=11072
    threadNumber=15925
    threadNumber=19725
    threadNumber=22336
    threadNumber=23933
    duration=11614 ms, done.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    结论

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

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

    如上表所示,

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

  • 相关阅读:
    前端经常遇到的手写js题
    一个完整的Flutter应用
    Ubuntu使用GParted增加swap分区后无法休眠解决办法
    安装vue-router及报错问题
    底层原理分析:探究SpringBoot底层对异常的处理机制
    Oracle(2-5)Usage and Configuration of the Oracle Shared Server
    DAY27:主机渗透测试
    WebRtc系列
    大数据学习(1)-Hadoop
    了解油封对汽车安全的影响?
  • 原文地址:https://blog.csdn.net/qq_41886200/article/details/133135779