头图

聊聊 Java 中的虚拟线程 VirtualThread

Java 中的虚拟线程 VirtualThread 从JDK 19开始引入的预览新特性,现在JDK已经发展到JDK 23了,虽然现在很多企业还是秉持着“你发任你发,我用Java8”的态度,但是作为一个Java程序员,还是要了解一下Java的新特性,毕竟Java是一个不断发展的语言,不断的更新迭代,我们也要跟着时代的步伐,不断的学习,不断的进步。

好的,言归正传,我们来聊聊 Java 中的虚拟线程 VirtualThread。在说 VirtualThread 之前,我们先了解一下 线程模型 ,也是对应我们的 传统线程虚拟线程 的理论基础。

线程模型

什么是线程模型?线程模型是指 用户线程内核线程 之间的关系,也就是 用户线程内核线程 是如何对应(映射)的。在Java中,我们通常使用的那些Thread对象里线程,其实是 用户线程,而 内核线程 是由操作系统来管理的,Java中的线程是由JVM来管理的,JVM会将Java中的线程映射到操作系统的线程上,这个映射关系就是 线程模型

(一):1-1(一个用户线程对应于一个系统(内核)线程)

1对1的线程模型,即一个用户线程对应一个系统(内核)线程,

优点: 这种模型的优点是实现简单;

缺点: 系统(内核)线程资源有限,如果一个应用程序中有大量的线程,那么系统(内核)线程的创建和销毁的开销大时影响系统性能。

图示:

img_3.png

(二)N-1(N个用户线程对应于一个系统(内核)线程)

N对1的线程模型,即N个用户线程对应一个系统(内核)线程,

优点: 这种模型的优点是节省了系统(内核)线程的创建和销毁的开销;

缺点: 结构相较于一对一模型较为复杂,有可能出现部分线程过忙,部分线程过闲的情况。

图示:

img_4.png

(三):N-M(N个用户线程对应于M个系统(内核)线程)

N对M的线程模型,即N个用户线程对应M个系统(内核)线程,

优点:
1:这种模型的优点是节省了系统(内核)线程的创建和销毁的开销;
2:一个用户线程可以对应多个系统(内核)线程,避免有的线程过忙或过闲;

缺点: 结构是这个三种模型中最复杂的,实现难度较大。

图示:

img_5.png

Java虚拟机使用的线程模型

我们把之前直接使用new Thread()创建的线程成为 平台线程,而 VirtualThread 称为 虚拟线程

(一):平台线程

平台线程使用的1对1的线程模型,即一个传统线程对应一个系统(内核)线程。
我们前面说1对1的模型的缺点是创建和销毁的开销影响性能,Java中引入了线程池的概念,线程池中的线程是可以复用的,这样就避免了线程的频繁创建和销毁,也就是达到了类似N对1的效果。

但不能像多对1那样,随时从a任务切换到b任务,必须要执行完a任务才能执行b任务,且当线程池中的线程数远大于CPU核心数时,内核线程的抢占式调度会导致线程上下文切换,这样会影响性能。

img_6.png

(二):虚拟线程

虚拟线程的思想是使用的的是N对M的线程模型,即N个虚拟线程对应M个系统(内核)线程。
这个模型的优点是可以更好的利用系统资源,避免了线程过多导致的系统资源的浪费,也避免了线程过少导致的线程过于繁忙。

最主要的是 虚拟线程轻量级 的,创建和销毁的开销很小,而且不会出现系统内核级别的线程上下文切换,这样就避免了线程上下文切换的开销。
如下图所示,虚拟线程其实也是一个任务,是在的线程池中执行的,线程池内的线程是我们的 平台线程 ,每个平台线程有自己的虚拟线程队列,当虚拟线程队列中的虚拟线程执行完毕后,会去执行其他虚拟线程队列中的虚拟线程,这样就避免有的线程过忙,有的线程过闲的情况。

img_7.png

这个线程池有什么特殊

眼尖的你可能已经发现了,这个线程池和我们之前说的线程池有什么不一样,这个线程池是 ForkJoinPool,它是Java7中引入的一个新的线程池,它的特点是 工作窃取
所以其实神秘的 虚拟线程 就是基于 ForkJoinPool 实现的,关于 ForkJoinPool 线程池我有一篇文章介绍过,有兴趣的可以看一下:【Java并发编程线程池】 ForkJoinPool 线程池是什么 怎么工作的 和传统的ThreadPoolExecutor比较

源码求证

我们来看一下 VirtualThread 的源码,看一下它是怎么实现的。我们使用oracle官方文档推荐的方式来创建一个 虚拟线程 并启动。

oracle 官方文档使用方式 https://docs.oracle.com/en/java/javase/21/core/virtual-thread...

// 创建一个虚拟线程并启动
Thread.ofVirtual().start(() -> System.out.println("Hello"));

跟着源码查看,在执行VirtualThread的start方法的时候,会吧虚拟线程当作任务放到scheduler中执行,再跟进看下这个scheduler的初始化

img_10.png

根据源码我们可以看见,这个scheduler是一个ForkJoinPool线程池,主要是通过这个scheduler来执行虚拟线程的任务,实现我们JDK的新特性 虚拟线程

ForkJoinPool从JDK7开始就有,就算公司用的JDK7/8,我也可以说我们也可以用虚拟线程啦!(哈哈,开个玩笑)。

img_11.png

总结

虚拟线程适合什么场景

  1. 适合短时间的任务,因为虚拟线程是轻量级的,创建和销毁的开销很小;
  2. 适合IO密集型的任务,因为虚拟线程不会出现系统内核级别的线程上下文切换,这样就避免了线程上下文切换的开销;
  3. 适合任务量大的场景,因为虚拟线程是轻量级的,可以更好的利用系统资源,避免了线程过多导致的系统资源的浪费,也避免了线程过少导致的线程过于繁忙。

测试用例

我们用sleep模拟100W个1秒的IO操作,看看虚拟线程和平台线程的性能对比。


public static void main(String[] args) throws InterruptedException {

    // 使用虚拟线程池
    ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
    executor(virtualThreadExecutor, "虚拟线程池");

    // 创建一个固定大小的线程池
    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
    executor(fixedThreadPool, "固定大小的线程池");

    // 创建一个缓存线程池
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    executor(cachedThreadPool, "缓存线程池");

    // 创建一个单线程的线程池
    ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
    executor(singleThreadPool, "单线程的线程池");

}

/**
 * 使用不同的线程池执行任务
 */
private static void executor(ExecutorService executor, String poolName) throws InterruptedException {

    long start = System.currentTimeMillis();

    for (int i = 0; i < 1_000_000; i++) {
        int finalI = i;
        executor.submit(() -> {
            try {
                performSimulatedIOOperation(finalI);
            } catch (Exception e) {
                System.out.println("Exception occurred: " + e.getMessage());
            }
        });
    }

    executor.shutdown();
    executor.awaitTermination(1, java.util.concurrent.TimeUnit.MINUTES);

    long end = System.currentTimeMillis();
    System.out.println("使用线程池" + poolName + "执行任务耗时:" + (end - start) + "ms");
}


/**
 * 模拟一个I/O操作
 */
private static void performSimulatedIOOperation(int index) throws InterruptedException {
    Thread.sleep(1000); // Simulating a one-second I/O operation
}

测试结果截图(抱歉结果太久,其他的线程池没执行玩就截图了,等不了了):

从测试结果可以看出,虚拟线程池的执行时间是最短的,这也验证了我们之前说的虚拟线程适合IO密集型的任务,适合任务量大的场景,适合需要高并发的场景。

img_12.png

从这个应用场景来看,虚拟线程可以用于高并发的场景,适合短时间的任务,适合IO密集型的任务,适合任务量大的场景。可以和netty做一个很好的结合,做一个高性能的网络应用。
为此我也写了一个简单的RPC框架,使用虚拟线程来实现高并发的场景,求求读者老爷们给个star✨??,地址在这里:JGZHAN/lrpc,后续会持续完善,欢迎大家一起交流学习。

好的,这就是我对Java中的虚拟线程 VirtualThread 的一些理解,希望对大家有所帮助,如果有疑问的地方,欢迎大家评论区留言讨论。

下期我会写一篇关于这个项目的文章,敬请期待,有兴趣的也可以和我一起来完善这个项目。

最后最后记得帮我的点个star✨,谢谢大家!
戳这里去点star
img_13.png


momo
1 声望0 粉丝