Java 21于2023年9月发布,根据New Relic发布的《2024年Java生态系统现状》报告可以得知以下信息:在Java 21发布的6个月内,有2.1%的应用在使用Java 21,这好像并不是很多,但对比目前最受欢迎的Java 17(目前有35%的应用在使用),当时发布的六个月内仅有0.37%的应用使用。
那么Java 21如此受欢迎的最大原因之一便是我们今天要介绍的内容——虚拟线程。
作为Java历史上最重要的创新之一,虚拟线程在Java 21成为正式特性,先看下它的发展历程:
一、什么是虚拟线程
先看几个线程术语:
操作系统线程(OS Thread):由操作系统管理,是操作系统调度的基本单位。
平台线程(Platform Thread):java.lang.Thread类的每个实例都是一个平台线程,是Java对OS Thread的抽象,1:1的关系。
虚拟线程(Vitrual Thread):一种轻量级、由JVM管理的线程,对应java.lang.VirtualThread类。
载体线程(Carrier Thread):指真正负责执行虚拟线程中任务的平台线程。一个虚拟线程装载倒一个平台线程后,那么这个平台线程就被称为虚拟线程的载体线程。
简单的说,传统的Java线程与OS线程是一对一的关系,由操作系统调度,数量自然也受限于操作系统,因此Java线程的创建和销毁都是比较“昂贵”的,虽然有池化技术(线程池)来缓解这个问题,但是也无法提升线程的数量,如下图:
而虚拟线程是Java实现的轻量级线程,不再和特定的OS线程绑定,而是由JVM管理,JVM调度程序通过平台线程(载体线程)来管理虚拟线程,多个虚拟线程可以共享一个平台线程,如下图:
这么一来,就有两个好处:
非常轻量级:多个虚拟线程可以映射到一个或极少量的OS线程上,这样可以避免大量的上下文切换。
线程不再“昂贵”:虚拟线程是由JVM自己管理的,不会直接占用操作系统的资源,是作为普通的Java对象存储在内存中的,并且单个虚拟线程实例占用的内存空间是byte级别的(单个平台线程是KB级别的),因此可以大量创建。
二、需要注意的几点
一个平台线程可以装载成百上千个虚拟线程,但有几点还是需要注意的:
虚拟线程总是守护线程,setDaemon(true)方法也不能把它变为非守护线程,所以当所有启动的非守护线程都终止时,JVM将终止。
虚拟线程不能设置优先级,虚拟线程始终具有normal优先级,即使使用setPriority()方法也不能更改。
虚拟线程不支持stop()、suspend()或resume()等方法,如果虚拟线程调用这些方法,会抛出UnsupportedOperationException异常。
三、如何创建
-
直接创建
void main() {
//创建并启动
Thread.startVirtualThread(()->{
System.out.println("virtual thread started");
});
}
-
通过Thread.Builder创建
void main() {
//虚拟线程
Thread vt = Thread.ofVirtual().name("虚拟线程").unstarted(() -> {
System.out.println("virtual thread unstarted");
});
//平台线程
Thread pt = Thread.ofPlatform().name("平台线程").unstarted(() -> {
System.out.println("platform thread unstarted");
});
//启动
vt.start();
pt.start();
}
-
通过虚拟线程的ThreadFactory创建
void main() {
ThreadFactory tf = Thread.ofVirtual().factory();
Thread vt = tf.newThread(() -> {
System.out.println("factory thread started");
});
vt.start();
}
-
通过线程池创建
try(var es = Executors.newVirtualThreadPerTaskExecutor()) {
es.submit(()->{
System.out.println("virtual thread started");
});
}
不过不推荐使用线程池来创建,JDK官方也多次提到不要将虚拟线程池化,这也好理解,线程池旨在共享昂贵的东西,但虚拟线程并不昂贵,所以也没必要池化。
四、性能对比
是骡子是马拉出来溜溜,说了半天,虚拟线程到底能提升多少性能,我们来做个测试。
我们写一个简单的任务,并行执行10000个休眠100ms的任务,我们先看平台线程的效果:
void main(){
Instant start = Instant.now();
try(var es = Executors.newFixedThreadPool(100)){
for (int i = 0; i < 10000; i++) {
es.submit(()->{
try {
//休眠100ms
Thread.sleep(Duration.ofMillis(100));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
}
Instant end = Instant.now();
System.out.println(STR."总耗时: \{Duration.between(start,end).toMillis()}");//总耗时: 10840
}
输出结果:
总耗时: 11131
总耗时大概11秒左右,我们再用虚拟线程跑跑看:
void main(){
AtomicInteger atomicInteger = new AtomicInteger();
Instant start = Instant.now();
try(var es=Executors.newVirtualThreadPerTaskExecutor()){
for (int i = 0; i < 10000; i++) {
es.submit(()->{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
System.out.println(atomicInteger.incrementAndGet());
}
}
Instant end = Instant.now();
System.out.println(STR."总耗时: \{Duration.between(start,end).toMillis()}");
}
输出结果:
总耗时: 412
大概有用0.4秒左右,11秒和0.4秒的差距,足以看出虚拟线程要比平台线程快得多。
五、传统线程池方案是否会被摒弃
看到这也许你有疑问,虚拟线程好像各方面都“碾压”平台线程,那么传统的线程池方案岂不是要被淘汰?这个也不能说的太满,线程池化技术确实是为了应对“资源紧张”的情况而生,现在虚拟线程使线程资源变得“廉价”了,但是它也不是万能的。
虚拟线程在处理I/O密集型任务时,可以显著提高系统吞吐量,但是对于那些CPU密集型的任务,比如把上述例子的休眠100ms变为100ms的计算(巨大的数组排序),即使把虚拟线程或者平台线程的数量加到很大也不会有明显的性能提升,因为虚拟线程不是更快的线程,它们运行代码的速度与平台线程相比并无优势,所以针对CPU密集型的任务,线程池依然可以很好的管理并发。
所以两者其实是可以共存的,线程池可以继续管理“有限”的工作线程,而虚拟线程可以大规模处理I/O密集型的任务。
End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。