大家好,我是程序员牛肉。
今天在闲看Open JKD的文档的时候,发现了这样一条来自官方的建议:虚拟线程永远都不应该被池化。
这份文档的链接我也放在这里,大家可以看一看,感觉有用的东西还是挺多的。
https://openjdk.org/jeps/425
这瞬间引起了我的好奇心:为什么Java的虚拟线程永远都不要池化呢?
在进入这个问题之前,我想我们要先介绍一下虚拟线程。
如果你学过Go或者Rust,相信你会对虚拟线程有一定的了解:这玩意不就是Go里面的协程嘛。
当我们在Java中创建一个普通线程的时候,实际上是需要映射到操作系统的线程中的。
因为通过常见的内核线程实现的方式在创建,调度线程的时候都需要内核参与。因此我们普通线程之间的线程切换需要操作系统从用户态切换到内核态。这种切换是极其浪费时间的,甚至有的时候这种切换耗时会超过任务本身执行所需时间。
而虚拟线程和操作系统的线程不是一一映射的,而是把多个虚拟线程与一个操作系统线程进行映射,并把这些虚拟线程交由给JVM进行管理,作为对象被存储。
平台线程:
虚拟线程:
在内存占用方面,虚拟线程相较于普通线程具有显著的优势。普通线程通常需要较多的内存来维持其运行状态,例如,每个Java普通线程默认会为其栈分配大约1MB的空间。而虚拟线程则轻量得多,它们的堆栈是作为堆对象存在的,并且可以被垃圾收集器回收,从而减少了内存占用。
具体到内存占用的量化比较,单个平台线程实例会占用2000多字节的数据,加上线程栈,总体占用空间至少是KB级别的。相比之下,虚拟线程实例则仅占用200到240字节的数据,加上其Continuation栈的内存占用,总体是byte级别的,远小于平台线程。
由此可以看来:虚拟线程相比较于普通的平台线程来讲,确实可以说是好用又廉价。
但虚拟线程不是万金油,从使用的角度上来讲,虚拟线程更加适合于处理IO密集型任务。由于虚拟线程是在一个线程中顺序执行的,因此不适合执行 CPU 密集型任务。
并且Java的虚拟线程也有一些自己的特点:
讲了这么多,让我们回到开头抛出的问题上:Open JDK 的维护者为什么不建议对虚拟线程采用池化技术?
相信看了前边关于对虚拟线程的介绍,你已经大致知道了答案:
[而池化技术对于线程来讲,本质上是为了避免线程被频繁创建和销毁所带来的性能消耗。但是虚拟线程实在是太廉价了,因此虚拟线程被频繁创建和销毁所带来的性能消耗并不是很高,所以没必要进行池化。]
但是官方只是不建议对虚拟线程池化,不代表虚拟线程不能池化。我们在最后借用一下池化技术看一看虚拟线程和普通线程的在IO层面的性能差异:
import java.util.concurrent.*;
import java.util.stream.IntStream;
public class IOIntensiveTaskComparison {
public static void main(String[] args) {
// 设置并发任务的数量
int numberOfTasks = 10000;
// 模拟IO密集型任务
Runnable ioIntensiveTask = () -> {
try {
// 模拟IO操作,例如数据库访问或网络请求
System.out.println("IO Task started by " + Thread.currentThread().getName());
Thread.sleep(100); // 模拟IO操作耗时100毫秒
System.out.println("IO Task completed by " + Thread.currentThread().getName());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
// 使用普通线程池执行IO密集型任务
long startTime = System.currentTimeMillis();
ExecutorService regularThreadPool = Executors.newFixedThreadPool(10);
try {
IntStream.range(0, numberOfTasks).forEach(i -> regularThreadPool.submit(ioIntensiveTask));
regularThreadPool.shutdown();
if (!regularThreadPool.awaitTermination(60, TimeUnit.SECONDS)) {
System.out.println("普通线程池任务并没有执行完");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
regularThreadPool.shutdownNow();
}
long endTime = System.currentTimeMillis();
System.out.println("Regular thread pool耗时: " + (endTime - startTime) + "毫秒");
// 重置开始时间
startTime = System.currentTimeMillis();
// 使用虚拟线程池执行IO密集型任务
ExecutorService virtualThreadPool = Executors.newVirtualThreadPerTaskExecutor();
try {
IntStream.range(0, numberOfTasks).forEach(i -> virtualThreadPool.submit(ioIntensiveTask));
virtualThreadPool.shutdown();
if (!virtualThreadPool.awaitTermination(60, TimeUnit.SECONDS)) {
System.out.println("虚拟线程任务并没有执行完");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
virtualThreadPool.shutdownNow();
}
endTime = System.currentTimeMillis();
System.out.println("Virtual thread pool耗时: " + (endTime - startTime) + "毫秒");
}
}
普通线程:
虚拟线程:
这么一比较确实有点吓人。在普通线程还没有执行完任务,虚拟线程执行完了任务的情况下,虚拟线程都比普通线程快了100倍。
让我们再试一下虚拟线程和普通线程同时执行CPU密集型任务:
package org.example;
import java.util.concurrent.*;
import java.util.stream.IntStream;
public class CPUIntensiveTaskComparison {
public static void main(String[] args) {
// 设置并发任务的数量
int numberOfTasks = 10000;
// 模拟CPU密集型任务
Runnable cpuIntensiveTask = () -> {
int result = 1;
int number = 1000; // 假设我们计算1000的阶乘,这是一个计算密集型任务
for (int i = 1; i <= number; i++) {
result *= i;
}
// 此处使用result变量以防止编译器优化掉循环
System.out.println("CPU Intensive Task completed by " + Thread.currentThread().getName());
};
// 使用普通线程池执行CPU密集型任务
long startTime = System.currentTimeMillis();
ExecutorService regularThreadPool = Executors.newFixedThreadPool(10);
try {
IntStream.range(0, numberOfTasks).forEach(i -> regularThreadPool.submit(cpuIntensiveTask));
regularThreadPool.shutdown();
if (!regularThreadPool.awaitTermination(60, TimeUnit.SECONDS)) {
System.out.println("普通线程池任务并没有执行完");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
regularThreadPool.shutdownNow();
}
long endTime = System.currentTimeMillis();
System.out.println("普通线程池耗时: " + (endTime - startTime) + "毫秒");
// 重置开始时间
startTime = System.currentTimeMillis();
// 使用虚拟线程池执行CPU密集型任务
// 注意:虚拟线程池不适合CPU密集型任务
ExecutorService virtualThreadPool = Executors.newVirtualThreadPerTaskExecutor();
try {
IntStream.range(0, numberOfTasks).forEach(i -> virtualThreadPool.submit(cpuIntensiveTask));
virtualThreadPool.shutdown();
if (!virtualThreadPool.awaitTermination(60, TimeUnit.SECONDS)) {
System.out.println("虚拟线程池任务并没有执行完");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
virtualThreadPool.shutdownNow();
}
endTime = System.currentTimeMillis();
System.out.println("虚拟线程池耗时: " + (endTime - startTime) + "毫秒");
}
}
普通线程:
虚拟线程:
在这里插一句:使用计算来模拟CPU密集型任务的时候,一定要输出计算结果,不然容易让编译器介入进行优化输出常量
我们可以看出虚拟线程在处理IO密集型任务的时候,相比较于普通线程有巨大的优势。但是在处理CPU密集型任务的时候,并没有多大的优势。
虚拟线程不是更快的线程;它们运行代码的速度并不比平台线程快。
它们的存在是为了提供规模的扩展(更高的吞吐量),而不是速度的优化(更低的延迟)。
相信通过我的介绍,你已经大致了解“Java的虚拟线程”。相信我的文章可以帮到你。
关于Java的虚拟线程,你有什么想说的吗?欢迎在评论区留言。