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 稍重。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值