JVM虚拟机如何生成百万级别线程

前言

以下代码案例可以分析出JVM虚拟机内部最多可以生成多少线程数量,电脑配置不同得到的实际结果有多差别。

测试代码

package com.feature.day01;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;

public class Playground {
    public static void main(String[] args) {
        var counter = new AtomicInteger();
        while (true) {
            new Thread(() -> {
                int count = counter.incrementAndGet();
                System.out.println("thread count = " + count);
                LockSupport.park();
            }).start();
        }
    }
}

运行结果

thread count = 2015
thread count = 2016
thread count = 2017
[0.508s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 4k, detached.
[0.508s][warning][os,thread] Failed to start the native thread for java.lang.Thread "Thread-2017"
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
	at java.base/java.lang.Thread.start0(Native Method)
	at java.base/java.lang.Thread.start(Thread.java:1535)
	at com.feature.day01.Playground.main(Playground.java:14)

结果分析

本机电脑配置

  1. MacBook Pro (13-inch, 2018, Four Thunderbolt 3 Ports)
  2. 2.3 GHz Intel Core i5
  3. 8 GB 2133 MHz LPDDR3

在当前电脑配置下,JVM虚拟机最多可以生成2017个线程,JVM栈空间的大小为默认1024k。从理论上来讲,影响JVM线程数量的因素有

  1. 栈空间大小

线程所占用的资源在JVM内部栈空间。因此,在内存固定的情况下,栈空间越小,理论上生成的线程数量越多(受操作系统限制);但是栈空间过小会引发两方面的问题:1. 递归深度不够;2. 栈空间不足 内存溢出

  1. JVM内存大小

跟上述的分析逻辑类似,JVM占用内存越大,分配给栈空间的内存区域也越大,因此理论上生成的线程数量也越大

  1. 操作系统限制

如上图所示,JVM线程跟操作系统线程是一一对应的关系,线程真正的执行需要等到OS分配资源进行调度。因此操作系统对线程数量的限制也影响JVM线程数量。Centos7 修改操作系统线程数量大小方法:

# vim /etc/security/limits.d/20-nproc.conf
* soft nproc 65535
* hard nproc 65535

虚拟线程

虚拟线程 java 虚拟线程数_System

回归正题,针对上述JVM线程限制的问题,JDK19提供虚拟线程的方式,可以在一定程度上突破线程数量大小的限制(借鉴了golang goroutine的设计思想)。在JDK19中线程分为平台线程(Platform Thread)、虚拟线程(Virtual Thread).

如上图所示,虚拟线程完全归JVM管理,不受操作系统限制,因此JVM可以生成大量的虚拟限制执行业务逻辑,从而提供系统的吞吐量。但是、但是、但是(重要的事情说三遍),该特性目前并不成熟,不建议在生产环境中使用。

代码案例

package com.feature.day01;

public class App01 {

    public static void main(String[] args) {
        for (int i = 0; i < 1_000_000; i++) {
            Thread.startVirtualThread(() -> {
                System.out.println(Thread.currentThread());
            });
        }
        System.out.println("hello world");
    }
}

运行以上代码需要开启虚拟机预览功能,如下图

虚拟线程 java 虚拟线程数_虚拟线程 java_02

执行结果

虚拟线程 java 虚拟线程数_java_03

从运行的结果中可以得出,虚拟线程内部是使用ForkJoinPool 线程池进行调度执行。

相关API

  1. Thread.ofVirtual() 创建虚拟线程
  2. Thread.ofPlatform() 创建平台线程
  3. Thread.startVirtualThread(Runnable) 创建并运行虚拟线程
  4. Thread.isVirtual() 判断是否为虚拟线程
  5. Thread.join / Thread.sleep 等待虚拟线程结束 / 使虚拟线程睡眠
    Runnable runnable = () -> System.out.println(Thread.sleep(10));
    Thread thread = Thread.startVirtualThread(runnable);
    thread.join();
  6. Executors.newVirtualThreadPerTaskExecutor()
    // 创建虚拟线程池 使用虚拟线程执行每一个Task
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> System.out.println(“hello”));
    }

守护进程

细节是魔鬼,不知道大家有没有注意到一个细节,为什么使用虚拟线程可以等待所有任务执行完成,但是平台线程就不行?

虚拟线程 java 虚拟线程数_System_04

如果使用平台线程执行完所有的任务(假设JVM能够创建如此多的线程数量),需要使用一些同步机制才能实现,如:CountDownLatch等,那么为什么使用虚拟线程就可以完整的输出Task任务呢?

final Thread thread = Thread.startVirtualThread(() -> {
  //System.out.println(Thread.currentThread() + "," );
});
System.out.println(Thread.currentThread() + "," + thread.isDaemon());
//运行结果显示 虚拟线程为守护线程,因此很好的解释了 为什么虚拟线程内部的Task可以执行完毕

平台线程 VS 虚拟线程

测试标准

测试内容比较简单,创建一万个线程,任务执行实际为1秒(睡眠一秒),并比较总执行时间和使用的系统线程数。

平台线程性能

//测试代码如下
package com.feature.day01;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class PlatformThreadPerformance {

    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
            ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);
            System.out.println(threadInfo.length + " os thread");
        }, 1, 1, TimeUnit.SECONDS);

        long l = System.currentTimeMillis();
        try(var executor = Executors.newFixedThreadPool(200)) {
            IntStream.range(0, 10000).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);
    }
}

//执行结果
//208 os thread
//elapsed time: 50220ms

结论:任务正常执行,创建208个系统线程,花费50220毫秒

虚拟线程性能

//测试代码

package com.feature.day01;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class VirTualThreadPerformance {

    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
            ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);
            System.out.println(threadInfo.length + " os thread");
        }, 1, 1, TimeUnit.SECONDS);

        long l = System.currentTimeMillis();
        try(var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            IntStream.range(0, 10000).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);
    }
}

//执行结果
// 17 os thread
// 17 os thread
// elapsed time: 2406ms

结论

  1. 虚拟线程执行时间为2406ms,产生17个系统线程
  2. 平台线程创建208个系统线程,花费50220ms

显而易见,虚拟线程执行的时间更快,且占用的系统资源更少。严格来说该测试代码并不严谨,平台线程使用了200个线程池做并发执行,有可能创建300、400个执行的时间会快一丢丢,但是创建的线程太多,线程上下文之间的切换也会花费时间,结果也并不一定好。但是该测试方案至少从某种程度上说明了虚拟线程的特点,即应对大吞吐量的任务请求具有明显的优势。

应用场景

到此为止,我们对虚拟线程有了一定的了解,那么需要思考一个问题,什么时候使用虚拟线程?首先需要明确虚拟线程的执行效率并不一定比平台线程高,如果有以下两个场景,可以考虑使用虚拟线程:

  1. 应用系统有大量的并发任务
  2. IO密集型场景,工作负载不受CPU限制