【Java】揭开虚拟线程的神秘面纱

一、什么是虚拟线程

虚拟线程(Virtual Threads)是Java 21版本中引入的一种新型线程模型,旨在简化并发编程并提高资源利用率。与传统的操作系统线程(平台线程)相比,虚拟线程是由Java虚拟机(JVM)管理的,它们的创建和调度更加高效,不需要直接映射到操作系统线程上。这意味着虚拟线程可以在同一时间处理更多的并发任务,而且由于它们的轻量级特性,虚拟线程的创建和管理开销远低于平台线程

虚拟线程的设计理念是通过共享线程池中的线程来避免重复创建和销毁线程的开销,从而提高资源的复用率。此外,虚拟线程的启动速度更快,适用于大量短期任务的场景,降低了等待线程启动的时间。由于虚拟线程的上下文切换更为轻量级,系统在切换不同任务之间的开销减少,提高了整体性能。因此,虚拟线程能够更好地处理大量细粒度的任务,提高了并发度和整体处理能力3.

在实际应用中,虚拟线程可以用于多种场景,包括高并发网络服务器、大规模数据处理、异步编程模型、实时数据流处理以及微服务架构中的并发处理等。这些应用场景通常涉及到大量的并发连接或独立任务,虚拟线程能够有效地管理这些并发请求,确保系统的高效性和响应能力1

public static void main(String[] args) {
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        IntStream.range(0, 10_000).forEach(i -> {
            executor.submit(() -> {
                Thread.sleep(Duration.ofSeconds(1));
                return i;
            });
        });
    } 
}

运行上面的代码看下执行时间,再试下 Executors.newFixedThreadPool(20) 和 Executors.newCachedThreadPool()

不出意外的话,会发现Executors.newVirtualThreadPerTaskExecutor()运行速度最快,Executors.newCachedThreadPool()运行时系统最卡顿,Executors.newFixedThreadPool(20) 最慢。

Executors.newCachedThreadPool()卡顿是因为一个任务创建一个Platform线程,占用了太多系统资源。

Executors.newFixedThreadPool(20)运行慢是因为只有20个并发去执行1万个任务

Executors.newVirtualThreadPerTaskExecutor()类似Executors.newCachedThreadPool(),但是创建的是虚拟线程,所以在获得高并发的同时也没有占用太多系统资源。

二、为什么引入虚拟线程

首先,我们来看看现在的Java线程是怎样的。

java.lang.Thread 这个类我相信大家都不陌生,代表Java中的最小并发单元,即一个线程。它是Java对底层的操作系统线程(OS Thread)的封装,为了区别于OS线程,我们称之为平台线程(Platform Thread)。当我们初始化一个Thread实例时,其实就是创建了一个Platform线程并将之与一个OS线程绑定(1:1)。

这种方式存在以下问题:

OS线程是有限的,Platform线程的创建数量受限制于OS线程
因为绑定系统资源,因此线程的创建/销毁的代价都是昂贵的
这两个问题并非无解,比如,问题1的本质是垂直扩展到顶了,完全可以用水平扩展的方式解决,一台机器的OS线程不能满足需求,再增加一台便是;问题2可以通过池化技术来解决,既然线程的创建和销毁代价比较昂贵,那便将创建好的线程收集起来,推迟销毁的时机,尽量复用它。

JDK21则是在语言层面上的提供了一个替代方案,也就是本文要介绍的虚拟线程(virtual thread),熟悉linux的同学肯定知道系统线程和用户线程的区别,虚拟线程就像是JDK实现的“用户线程”,下面来重点介绍。

什么是虚拟线程
虚拟线程,可以看作是对Platform线程的轻量级封装,Platform线程和OS线程的关系是1:1,虚拟线程和Platform线程的关系则是M:N,且一般M要远远大于N。

可以直接看下虚拟线程的构造函数源码加深理解,坐标java.lang.VirtualThread#。

三、虚拟线程实例化

final class VirtualThread extends BaseVirtualThread {
    VirtualThread(Executor scheduler, String name, int characteristics, Runnable task) {
        super(name, characteristics, /*bound*/ false);
        Objects.requireNonNull(task);

        // choose scheduler if not specified
        if (scheduler == null) {
            Thread parent = Thread.currentThread();
            if (parent instanceof VirtualThread vparent) {
                scheduler = vparent.scheduler;
            } else {
                scheduler = DEFAULT_SCHEDULER;
            }
        }

        this.scheduler = scheduler;
        this.cont = new VThreadContinuation(this, task);
        this.runContinuation = this::runContinuation;
    }
}

private static ForkJoinPool createDefaultScheduler() {
        ForkJoinWorkerThreadFactory factory = pool -> {
            PrivilegedAction<ForkJoinWorkerThread> pa = () -> new CarrierThread(pool);
            return AccessController.doPrivileged(pa);
        };
        PrivilegedAction<ForkJoinPool> pa = () -> {
            int parallelism, maxPoolSize, minRunnable;
            String parallelismValue = System.getProperty("jdk.virtualThreadScheduler.parallelism");
            String maxPoolSizeValue = System.getProperty("jdk.virtualThreadScheduler.maxPoolSize");
            String minRunnableValue = System.getProperty("jdk.virtualThreadScheduler.minRunnable");
            if (parallelismValue != null) {
                parallelism = Integer.parseInt(parallelismValue);
            } else {
                parallelism = Runtime.getRuntime().availableProcessors();
            }
            if (maxPoolSizeValue != null) {
                maxPoolSize = Integer.parseInt(maxPoolSizeValue);
                parallelism = Integer.min(parallelism, maxPoolSize);
            } else {
                maxPoolSize = Integer.max(parallelism, 256);
            }
            if (minRunnableValue != null) {
                minRunnable = Integer.parseInt(minRunnableValue);
            } else {
                minRunnable = Integer.max(parallelism / 2, 1);
            }
            Thread.UncaughtExceptionHandler handler = (t, e) -> { };
            boolean asyncMode = true; // FIFO
            return new ForkJoinPool(parallelism, factory, handler, asyncMode,
                         0, maxPoolSize, minRunnable, pool -> true, 30, SECONDS);
        };
        return AccessController.doPrivileged(pa);
    }

可以看到,创建虚拟线程的时候,使用了一个默认的调度器(ForkJoinPool),也就是Platform的线程池,可以看到池子的几个配置参数。

最大Platform线程数:默认为系统核心数,最大为256,可以通过jdk.virtualThreadScheduler.maxPoolSize设置
这个时候,爱思考的同学可能就要问了,既然默认的最大Platform线程数为系统核心数,岂不是大大限制了并发能力?是不是要主动设置一个较大值?

答案是不需要,因为JDK在线程池的基础上实现了调度的功能。当虚拟线程启动时,调度器会将虚拟线程mount到Platform线程,此时该Platform线程被称为这个虚拟线程的carrier;当线程运行遇到IO操作需要等待时,调度器又会将虚拟现场unmount,把Platform线程释放出来给其他虚拟线程使用,不占用CPU时间。因此,对于非CPU密集的应用,很少的Platform线程就能支持大量的虚拟线程来执行任务。事实上,对于CPU密集的应用,虚拟线程并不会带来多大的提升。虚拟线程真正的应用场景是生存周期短、调用栈浅的任务,如一次http请求、一次JDBC查询。

需要明确的是,操作系统真正能同时运算的线程数也就只有逻辑CPU数,多出来的线程只能等待系统的调度获得CPU时间。

四、虚拟线程状态

虚拟线程状态如下:

  • NEW
  • STARTED
  • TERMINATED
  • RUNNING
  • PARKING
  • PARKED
  • PINNED
  • UNPARKED

可以看出,虚拟线程相较原先的线程状态,多了Parked、Unparked、Pinned等状态

Parked:就是前面说的mount

Unparked:就是前面说的unmount

Pinned:虚拟线程阻塞时,正常会unmount,但是在一些特殊场景下,不能unmount,此时就会进入Pinned状态:

阻塞操作在 synchronized 代码块中(后续JDK可能优化这一点限制)
执行 native 方法时
Pinned状态占用了Platform线程,无疑会影响性能,官方建议对于经常执行的 synchronized 代码块,最好使用java.util.concurrent.locks.ReentrantLock 替代。如果不清楚自己代码里哪些地方使用到了 synchronized 代码块,在切换使用虚拟线程时,可以添加JVM参数jdk.tracePinnedThreads帮助排查。

总结

虚拟线程特别适用如下场景:有大量的并发任务需要执行,且任务是非CPU密集的。

虚拟线程使用上和普通的线程没有太大区别,甚至因为内置了调度逻辑和线程池,可以让开发人员不用再考虑线程池的大小、拒绝策略等,尤其给框架开发者提供了新的优化思路。

对于已经使用了reactive技术的如webFlux框架,没必要再切换到虚拟线程,两者性能相当。

对于web容器如tomcat来说,本身已经使用reactor、nio等技术优化吞吐量,在小的并发数场景下,没必要切换虚拟线程,提升不大。

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
### 回答1: Java虚拟线程是一种伪线程,它是由用户线程在单个物理线程上调度的。这意味着,即使系统有多个虚拟线程,也只能在单个物理线程上运行其中一个虚拟线程。这样做的优点是可以在单个线程上执行多个任务,但缺点是虚拟线程无法充分利用多核系统的计算能力。 虚拟线程通常用于轻量级任务或其他不需要多线程并行执行的情况。例如,它可以用于网络服务器中的连接,以便在单个线程上处理多个客户端连接。 ### 回答2: Java虚拟线程是一种由Java虚拟机(JVM)管理和调度的线程。它们被称为虚拟线程,是因为它们是在JVM中进行管理和调度的,并不是直接由操作系统创建的原生线程Java虚拟线程有以下特点: 1. 轻量级:与操作系统的线程相比,Java虚拟线程是轻量级的,创建和销毁的成本较低。这使得可以创建大量的虚拟线程,从而支持多线程的并发执行。 2. 可移植性:由于虚拟线程是由JVM管理的,所以它们具有高度的可移植性。无论在哪个操作系统上运行Java程序,虚拟线程都可以正常工作,而不需要重新编写代码。 3. 线程安全性:Java虚拟线程会自动受到JVM的管理和保护,因此不容易出现线程安全性问题。当多个线程同时访问共享资源时,JVM会提供内置的线程同步机制,如锁和信号量,以确保数据的一致性和可靠性。 4. 高效性:虚拟线程在执行上的效率高于操作系统的线程。这是因为JVM可以使用各种优化措施,如线程池、就绪队列和调度算法,来提高线程的执行效率。 总的来说,Java虚拟线程是一个强大且灵活的多线程机制。它提供了方便的线程管理和调度功能,并且在不同平台和操作系统上表现一致。对于开发并发应用程序和提高系统性能而言,Java虚拟线程是一个非常有价值的工具。 ### 回答3: Java虚拟线程是指通过Java虚拟机(JVM)来进行调度和执行的线程。与操作系统的原生线程相比,虚拟线程具有以下优点: 1. 轻量级:虚拟线程是基于用户空间的线程,它们不需要操作系统的介入,因此创建和切换的开销相对较小,可以更高效地利用系统资源。 2. 独立性:虚拟线程是独立于操作系统线程的,这意味着在一个虚拟线程被阻塞时,不会影响其他虚拟线程的执行。 3. 灵活性:Java虚拟线程模型提供了丰富的线程管理和调度方法,例如使用线程池可以管理大量线程的创建和销毁,可按需动态调整线程数量,避免资源浪费。 4. 跨平台性:Java虚拟线程可以在不同的操作系统上运行,只要虚拟机支持该平台即可,这大大提升了代码的可移植性和跨平台性。 5. 安全性:Java虚拟线程模型通过提供线程同步机制和异常处理机制来保证多线程的安全执行,避免了线程对共享资源的竞争和冲突,提高程序稳定性和可靠性。 需要注意的是,虚拟线程与操作系统的原生线程之间存在一定的性能差距,因为虚拟线程需要通过JVM进行调度和执行。在某些高性能应用场景下,可能需要使用操作系统级别的线程来提高性能。此外,虚拟线程的创建和销毁由JVM来管理,稍有不慎就可能导致线程泄漏或占用过多的系统资源,因此需要合理使用和管理虚拟线程
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

从零开始的-CodeNinja之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值