Java19 带来的虚拟线程是怎样玩出花提升十倍性能的

我们都知道一个 Java 服务其实是一个进程,当这个服务遇到高并发场景的时候我们往往会考虑使用线程池来提高性能,JDK 中就自带线程池,关于 JDK 的线程池感兴趣可以去看一阿粉之前的文章 聊聊面试中的 Java 线程池。今天阿粉想跟大家聊的时候 Java19 中提到的虚拟线程 virtual threads

基本概念

我们都知道 Java 中的线程跟操作系统的内核线程是一对一的,Java 线程的调度其实是依赖操作系统的内核线程的,这就导致了我们的线程切换和运行就需要进行上下文切换以及消耗大量的系统资源,同时我们也知道机器的资源是昂贵的并且也是有限的,我们不能也无法肆无忌惮的创建线程,因此线程往往会成为我们系统的瓶颈。

008vxvgGgy1h752nxsa3gj30u0071jru

为了解决这个问题,Java19 中提出了一种虚拟线程的概念,为了区别,之前的线程被称为平台线程。要注意虚拟线程并不是用来直接取代平台线程的,虚拟线程是建立在平台线程之上的,一个平台线程可以对应多个虚拟线程,同时一个平台线程还是一一对应内核线程,因此上面的架构就变成了如下,一个 VT 代表一个虚拟线程。

008vxvgGgy1h753ur4g5jj30qq0ba758

如果有小伙伴对 GO 语言比较熟悉的话,就会想到 Java 中的虚拟线程跟 GO 中的 Goroutines 是很类似的,确实是这样,所以说语言都是相通的。

举个栗子

这里我们通过分别使用平台线程以及虚拟线程来测试一个 case 看看两者的耗时和性能是怎样的,测试分如下几步,我们依次来看一下。注意下面的测试代码都是在 Java19 的版本中运行的

平台线程方式

我们通过 JDK 自带的线程池 Executors.newCachedThreadPool() 来创建线程池,并执行一定数据任务,任务的数量我们通过入参来控制,方便后续通过主函数调用。

public static void platformThread(int size) {
    long l = System.currentTimeMillis();
    try(var executor = Executors.newCachedThreadPool()) {
      IntStream.range(0, size).forEach(i -> {
        executor.submit(() -> {
          Thread.sleep(Duration.ofSeconds(1));
          //System.out.println(i);
          return i;
        });
      });
    }
    System.out.printf("elapsed time: %dms\n", System.currentTimeMillis() - l);
  }

虚拟线程的方式

虚拟线程的代码跟上面的代码十分相似,代码如下。可以看到,在代码层面上跟上面唯一的区别就是 Executors.newCachedThreadPool() 这一行变成了 Executors.newVirtualThreadPerTaskExecutor() 即代表创建的虚拟线程。

  public static void virThread(int size) {
    long l = System.currentTimeMillis();
    try(var executor = Executors.newVirtualThreadPerTaskExecutor()) {
      IntStream.range(0, size).forEach(i -> {
        executor.submit(() -> {
          Thread.sleep(Duration.ofSeconds(1));
          //System.out.println(i);
          return i;
        });
      });
    }
    System.out.printf("elapsed time: %dms\n", System.currentTimeMillis() - l);
  }

监控运行的线程

上面的两个方法都是都是创建线程池用来提交任务的,但是位于具体创建了多少个线程我们是不知道的,所以我们还需要通过下面的代码来监控。

public static void main(String[] args) {
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
    scheduledExecutorService.scheduleAtFixedRate(() -> {
      ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
      ThreadInfo[] threadInfo = threadBean.dumpAllThreads(falsefalse);
      long count = Arrays.stream(threadInfo).count();
      System.out.println(count + " os thread");
    }, 11, TimeUnit.SECONDS);
  
    int size = 100000;
//    platformThread(size);
    virThread(size);
  }

通过另一个线程池开启一个线程信息监控的线程,每秒钟输出一次当前的运行线程数。这里注意,如果上面的代码在 IDEA 中提示报错,找不到类,如下所示,我们可以将鼠标放上去进行修复。

008vxvgGgy1h756u8k3goj31sm0haq8u

也可以手动在设置中的编译器》Java 编译器这里给自己的模块增加一个编译参数 -parameters --add-modules java.management --enable-preview

008vxvgGgy1h756v6qem3j31a70u00wl

运行

上面的三段组合在一起就是一个完整的 case,如果这个时候如果上面的代码都正常,在运行的时候不出意外会出现下面的错误,

008vxvgGgy1h756xs7syhj32550u0tgq

这里是因为当前 Java19 中的虚拟线程特性还处于预览阶段,不能直接使用,我们需要在启动参数上面配置 --enable-preview 参数,才能正常测试,如下所示,不同版本的 IDEA 可能显示的位置不一样,但是都是配置 VM 参数,找一下就好了。

008vxvgGgy1h7570v6n0jj31cs0u0jv0

配置好了过后再次运行就可以得到如下的结果,可以看到在 size 大小为 100000 的情况下,虚拟线程只创建了 12 个平台线程,并且只在 2523 ms 就完成了整个任务。

008vxvgGgy1h7572po1p6j31hj0u0dlf

但是当我们运行平台线程的方法的时候会发现,同样的 size 的情况下,平台线程创建了好几千个,而且还会触发 OOM,因为操作系统的资源已经被耗尽了,由此可见虚拟线程的性能要远远高于平台线程。YYDS!

008vxvgGgy1h757599lsvj31by0u07b1
为了避免 OOM 我们也可以将代码中的 Executors.newCachedThreadPool() 方法,改成 Executors.newFixedThreadPool(xxx),这样虽然可以避免大量创建线程导致 OOM,但是任务执行的时长就会消耗更长,阿粉这边测试在 size 为 10000 的情况下,配置 500 个线程的时候,总共花费了 20276 ms,在数据量小十倍的情况下耗时却增长十倍。性能可想而知,感兴趣的小伙伴可以自己尝试一下。
008vxvgGgy1h757loulynj31610u0juv

标签


沙漏洒洒

关注公众号,获取程序化广告相关资料

回复关键字:计算广告

发表评论