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)
结果分析
本机电脑配置
- MacBook Pro (13-inch, 2018, Four Thunderbolt 3 Ports)
- 2.3 GHz Intel Core i5
- 8 GB 2133 MHz LPDDR3
在当前电脑配置下,JVM虚拟机最多可以生成2017个线程,JVM栈空间的大小为默认1024k。从理论上来讲,影响JVM线程数量的因素有
- 栈空间大小
线程所占用的资源在JVM内部栈空间。因此,在内存固定的情况下,栈空间越小,理论上生成的线程数量越多(受操作系统限制);但是栈空间过小会引发两方面的问题:1. 递归深度不够;2. 栈空间不足 内存溢出
- JVM内存大小
跟上述的分析逻辑类似,JVM占用内存越大,分配给栈空间的内存区域也越大,因此理论上生成的线程数量也越大
- 操作系统限制
如上图所示,JVM线程跟操作系统线程是一一对应的关系,线程真正的执行需要等到OS分配资源进行调度。因此操作系统对线程数量的限制也影响JVM线程数量。Centos7 修改操作系统线程数量大小方法:
# vim /etc/security/limits.d/20-nproc.conf
* soft nproc 65535
* hard nproc 65535
虚拟线程
回归正题,针对上述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");
}
}
运行以上代码需要开启虚拟机预览功能,如下图
执行结果
从运行的结果中可以得出,虚拟线程内部是使用ForkJoinPool 线程池进行调度执行。
相关API
- Thread.ofVirtual() 创建虚拟线程
- Thread.ofPlatform() 创建平台线程
- Thread.startVirtualThread(Runnable) 创建并运行虚拟线程
- Thread.isVirtual() 判断是否为虚拟线程
- Thread.join / Thread.sleep 等待虚拟线程结束 / 使虚拟线程睡眠
Runnable runnable = () -> System.out.println(Thread.sleep(10));
Thread thread = Thread.startVirtualThread(runnable);
thread.join(); - Executors.newVirtualThreadPerTaskExecutor()
// 创建虚拟线程池 使用虚拟线程执行每一个Task
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> System.out.println(“hello”));
}
守护进程
细节是魔鬼,不知道大家有没有注意到一个细节,为什么使用虚拟线程可以等待所有任务执行完成,但是平台线程就不行?
如果使用平台线程执行完所有的任务(假设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
结论
- 虚拟线程执行时间为2406ms,产生17个系统线程
- 平台线程创建208个系统线程,花费50220ms
显而易见,虚拟线程执行的时间更快,且占用的系统资源更少。严格来说该测试代码并不严谨,平台线程使用了200个线程池做并发执行,有可能创建300、400个执行的时间会快一丢丢,但是创建的线程太多,线程上下文之间的切换也会花费时间,结果也并不一定好。但是该测试方案至少从某种程度上说明了虚拟线程的特点,即应对大吞吐量的任务请求具有明显的优势。
应用场景
到此为止,我们对虚拟线程有了一定的了解,那么需要思考一个问题,什么时候使用虚拟线程?首先需要明确虚拟线程的执行效率并不一定比平台线程高,如果有以下两个场景,可以考虑使用虚拟线程:
- 应用系统有大量的并发任务
- IO密集型场景,工作负载不受CPU限制