JDK21虚拟线程、OS线程、GO协程性能对比测试
JDK21虚拟线程、OS线程、GO协程性能对比测试
JDK21引入的虚拟线程是一种轻量级线程实现,旨在减少多线程编程的复杂性并提高应用程序的并发性能。本文通过与传统OS线程和GO协程的性能对比测试,深入探讨了虚拟线程的工作原理和实际应用效果。
Java虚拟线程(Virtual Threads)是Java的一种轻量级线程实现,于Java 19通过预览功能引入,并在Java 21中正式稳定。它旨在减少多线程编程的复杂性并提高应用程序的并发性能。虚拟线程的目标是提供与传统平台线程(OS线程)一样的语义,但以更低的资源开销运行更多线程。
一、与平台线程的区别
- 传统的Java线程(Platform Thread)由操作系统直接管理,每个线程占用一个OS线程。
- 虚拟线程是JVM层实现的轻量级线程,它由JVM调度,而不是由操作系统调度。多个虚拟线程可以映射到一个或多个平台线程上运行。
二、工作原理
- 线程池模式:JVM维护一个线程池,其中包含少量的后台平台线程。虚拟线程以任务的形式提交到线程池中。
- 栈管理:虚拟线程的栈是动态分配的,可以根据需要扩展和收缩,而不像OS线程那样需要预分配大块内存。
- 上下文切换:虚拟线程的上下文切换发生在JVM内,代价比OS线程的上下文切换低得多。
- 阻塞操作处理:
阻塞操作(如IO)不会阻塞底层的OS线程,而是挂起虚拟线程并将OS线程释放给其他任务。
当阻塞操作完成时,虚拟线程会重新调度运行。
这通过线程挂起(Continuation)机制实现,JVM可以暂停和恢复虚拟线程的执行状态。
三、常规运算性能对比
模拟常规运算,分别创建20000个线程执行
JDK21 虚拟线程 OS线程
public static void testTraditionalThreads(int taskCount) throws InterruptedException {
Thread[] threads = new Thread[taskCount];
for (int i = 0; i < taskCount; i++) {
threads[i] = new Thread(() -> performTask());
threads[i].start();
}
// 等待所有线程完成
for (Thread thread : threads) {
thread.join();
}
}
public static void testVirtualThreads(int taskCount) throws InterruptedException {
var executor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual().factory());
for (int i = 0; i < taskCount; i++) {
executor.execute(() -> performTask());
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
}
private static void performTask() {
// 模拟一个简单的计算任务
long sum = 0;
for (int i = 0; i < 1_000; i++) {
sum += i;
}
}
运行结果
10000个任务时,差距大概3.58倍
Traditional threads took: 1257 ms
Virtual threads took: 351 ms
Time difference: 906 ms
20000个任务时:差距大概4.66倍
Traditional threads took: 3086 ms
Virtual threads took: 662 ms
Time difference: 2424 ms
再来看看CPU占用及创建OS线程数量,由于传统OS线程的特性,资源占用也不是一个量级的
传统线程:创建了几千个OS线程,CPU占用也飙升到78%
虚拟线程:创建的OS线程数量不超过10个,CPU占用最高不超过6%
再来对比看下更明显:前一次是传统线程方法调用,后一次是虚拟线程方法调用
GO协程
func performTask() {
var sum int64 = 0
for i := 0; i < 1000; i++ {
sum += int64(i)
}
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) // 最大化 CPU 使用率
const numTasks = 10000 // 任务数量
var wg sync.WaitGroup
wg.Add(numTasks)
start := time.Now()
for i := 0; i < numTasks; i++ {
go func() {
defer wg.Done()
performTask()
}()
}
wg.Wait()
duration := time.Since(start)
fmt.Printf("Completed %d tasks in %v\n", numTasks, duration)
}
Completed 10000 tasks in 3.5ms
性能比Java 虚拟线程高接近百倍
四、HTTP调用性能对比
这次换了12600KF处理器的电脑来测试。性能比上面好一些。
HTTP GET 请求从雪球网页版获取某个股票的成交数据。查询2000次。
OS线程
资源消耗情况:
耗时:29928 ms
虚拟线程
资源消耗情况:
耗时:30755 ms
最后放一张对比,前一次是虚拟线程池,后一次是操作系统线程池
GO 协程
func workerPool(workerID int, jobs <-chan int, wg *sync.WaitGroup, client *http.Client) {
defer wg.Done()
for job := range jobs {
fetch(job, client)
}
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) // 最大化 CPU 利用
numWorkers := runtime.NumCPU() * 2 // 根据 CPU 核心数设置最佳并发量
jobs := make(chan int, numRequests)
var wg sync.WaitGroup
client := &http.Client{}
// 启动 Worker Pool
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go workerPool(i, jobs, &wg, client)
}
// 发送 2000 个任务
start := time.Now()
for i := 0; i < numRequests; i++ {
jobs <- i
}
close(jobs)
wg.Wait()
duration := time.Since(start)
fmt.Printf("Completed %d requests in %v using %d workers\n", numRequests, duration, numWorkers)
}
Completed 2000 requests in 5.173660179s using 16 workers
HTTP调用总结:
- 使用传统线程池,会创建巨量的操作系统线程,CPU使用率更优,时间上快一些。耗时:29928 ms
- 虚拟线程池创建数量极少的操作系统线程,但是CPU使用略高一些。时间上慢。耗时:30755 ms
- GO 协程,性能最佳,仅耗时5173ms
总结
- GO协程在CPU密集型任务和IO密集型任务上的表现都要优于JDK的线程
- Go 的 goroutine 仍然比 JDK 21 的虚拟线程更高效
Go 的 M:N 线程调度模型比 Java 的 ForkJoinPool 更轻量,goroutine 切换成本更低。
Java 仍然有 JVM 开销(GC、线程管理等),导致 任务切换稍微慢一些。 - JDK 21 虚拟线程已经远远快于传统 Java 线程
以前 Java 线程是 1:1 映射到 OS 线程,创建 1w 线程会崩溃。
现在的虚拟线程使 Java 能够 创建数百万级轻量线程,但仍比 Go goroutine 稍重。