虚拟线程是Java 19提出并在java21正式引入的一项革命性并发特性,作为Project Loom项目的核心成果,它彻底改变了Java处理高并发场景的方式。其是JVM管理的轻量级线程,与传统操作系统线程(即平台线程)相比具有本质区别。它采用M:N调度模型,即多个虚拟线程(M)映射到少量平台线程(N)上执行,由JVM而非操作系统负责调度。这种设计使虚拟线程具有以下核心特性:
轻量级:每个虚拟线程仅需约1KB内存,而传统线程通常需要1MB栈空间
高并发:可轻松创建数百万个虚拟线程,突破物理线程限制
低成本:虚拟线程的创建和切换开销极低,上下文切换由JVM在用户空间完成
兼容性:其保持与传统Thread API相同的编程模型,学习成本低
虚拟线程是基于Continuation(延续)机制实现,当其遇到阻塞操作(如I/O)时,JVM会将虚拟线程状态保存到堆内存并挂起,释放载体线程执行其他任务;当阻塞操作完成,再从堆中恢复状态继续执行。这种"挂起-恢复"机制使得: I/O等待不再浪费线程资源;少量载体线程可服务大量并发请求;编程模型保持同步风格,但获得异步性能。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadComparisonDemo {
// 总任务数
private static final int TASK_COUNT = 10000;
// 每个任务的模拟处理时间(毫秒)
private static final int TASK_PROCESSING_TIME = 100;
public static void main(String[] args) {
System.out.println("开始线程性能对比测试");
System.out.println("任务数量: " + TASK_COUNT + " | 每个任务处理时间: " + TASK_PROCESSING_TIME + "ms");
try {
testPlatformThreads(); // 测试传统线程池
testVirtualThreads(); // 测试虚拟线程
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("测试被中断");
}
}
private static void testPlatformThreads() throws InterruptedException {
System.out.println("\n===== 测试传统线程池 =====");
// 传统线程使用固定大小的线程池(通常设置为CPU核心数)
int poolSize = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(poolSize);
System.out.println("线程池大小: " + poolSize);//注意:传统线程池受限于线程数量,任务将排队等待执行
CountDownLatch latch = new CountDownLatch(TASK_COUNT);
long startTime = System.currentTimeMillis();
// 提交所有任务
for (int i = 0; i < TASK_COUNT; i++) {
final int taskId = i;
executor.execute(() -> {
try {
Thread.sleep(TASK_PROCESSING_TIME);// // 模拟任务处理(I/O等待、数据库操作等)
if (taskId % 1000 == 0) { // 每完成1000个任务打印一次进度
System.out.printf("传统线程池 - 已完成任务: %,d/%d%n",
TASK_COUNT - latch.getCount(), TASK_COUNT);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
}
latch.await();// 等待所有任务完成
long endTime = System.currentTimeMillis();
executor.shutdown();
System.out.println("传统线程池 - 所有任务完成");
System.out.printf("总耗时: %,d 毫秒%n", (endTime - startTime));
System.out.printf("平均每个任务耗时: %.2f 毫秒%n",
(double)(endTime - startTime) / TASK_COUNT);
}
private static void testVirtualThreads() throws InterruptedException {
System.out.println("\n===== 测试虚拟线程 =====");//虚拟线程将为每个任务创建独立线程,无数量限制
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();// 使用虚拟线程执行器
CountDownLatch latch = new CountDownLatch(TASK_COUNT);
long startTime = System.currentTimeMillis();
for (int i = 0; i < TASK_COUNT; i++) {// 提交所有任务
final int taskId = i;
executor.execute(() -> {
try {
Thread.sleep(TASK_PROCESSING_TIME);// 模拟任务处理
if (taskId % 1000 == 0) { // 每完成1000个任务打印一次进度
System.out.printf("虚拟线程 - 已完成任务: %,d/%d (线程: %s)%n",
TASK_COUNT - latch.getCount(), TASK_COUNT,
Thread.currentThread());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
}
latch.await();// 等待所有任务完成
long endTime = System.currentTimeMillis();
executor.close();
System.out.println("虚拟线程 - 所有任务完成");
System.out.printf("总耗时: %,d 毫秒%n", (endTime - startTime));
System.out.printf("平均每个任务耗时: %.2f 毫秒%n",
(double)(endTime - startTime) / TASK_COUNT);
System.out.println("\n虚拟线程示例信息:");// 打印虚拟线程信息
System.out.println("最后一个虚拟线程: " + Thread.currentThread());
System.out.println("是否为虚拟线程: " + Thread.currentThread().isVirtual());
}
}
其运行结果如下:
开始线程性能对比测试
任务数量: 10000 | 每个任务处理时间: 100ms
===== 测试传统线程池 =====
线程池大小: 16
传统线程池 - 已完成任务: 14/10000
传统线程池 - 已完成任务: 1,004/10000
传统线程池 - 已完成任务: 2,011/10000
传统线程池 - 已完成任务: 2,999/10000
传统线程池 - 已完成任务: 4,003/10000
传统线程池 - 已完成任务: 4,998/10000
传统线程池 - 已完成任务: 6,009/10000
传统线程池 - 已完成任务: 6,997/10000
传统线程池 - 已完成任务: 8,011/10000
传统线程池 - 已完成任务: 9,006/10000
传统线程池 - 所有任务完成
总耗时: 68,897 毫秒
平均每个任务耗时: 6.89 毫秒
===== 测试虚拟线程 =====
虚拟线程 - 已完成任务: 3/10000 (线程: VirtualThread[#46]/runnable@ForkJoinPool-1-worker-13)
虚拟线程 - 已完成任务: 1,000/10000 (线程: VirtualThread[#1063]/runnable@ForkJoinPool-1-worker-10)
虚拟线程 - 已完成任务: 2,000/10000 (线程: VirtualThread[#2063]/runnable@ForkJoinPool-1-worker-4)
虚拟线程 - 已完成任务: 3,000/10000 (线程: VirtualThread[#3063]/runnable@ForkJoinPool-1-worker-1)
虚拟线程 - 已完成任务: 4,000/10000 (线程: VirtualThread[#4070]/runnable@ForkJoinPool-1-worker-5)
虚拟线程 - 已完成任务: 5,001/10000 (线程: VirtualThread[#5070]/runnable@ForkJoinPool-1-worker-2)
虚拟线程 - 已完成任务: 6,001/10000 (线程: VirtualThread[#6070]/runnable@ForkJoinPool-1-worker-10)
虚拟线程 - 已完成任务: 7,000/10000 (线程: VirtualThread[#7070]/runnable@ForkJoinPool-1-worker-16)
虚拟线程 - 已完成任务: 8,001/10000 (线程: VirtualThread[#8070]/runnable@ForkJoinPool-1-worker-3)
虚拟线程 - 已完成任务: 9,001/10000 (线程: VirtualThread[#9070]/runnable@ForkJoinPool-1-worker-11)
虚拟线程 - 所有任务完成
总耗时: 170 毫秒
平均每个任务耗时: 0.02 毫秒
虚拟线程示例信息:
最后一个虚拟线程: Thread[#1,main,5,main]
是否为虚拟线程: false
Process finished with exit code 0
可以看到,传统的线程受限于cpu核心数量,在相同的条件下。其平均耗时和总耗时远高于虚拟线程。在上面代码的基础上增加一个内存监视后
private static void printMemoryUsage(String phase) {
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
System.out.printf("[内存使用] %s - 已用: %,d MB, 总量: %,d MB, 最大: %,d MB%n",
phase,
usedMemory / (1024 * 1024),
runtime.totalMemory() / (1024 * 1024),
runtime.maxMemory() / (1024 * 1024));
}
其改进后的结果如下:
开始线程性能对比测试
任务数量: 10000 | 每个任务处理时间: 100ms
===== 测试传统线程池 =====
线程池大小: 16
[内存使用] 线程池创建后 - 已用: 3 MB, 总量: 244 MB, 最大: 3,904 MB
传统线程池 - 已完成任务: 11/10000
[内存使用] 任务执行中 - 已用: 8 MB, 总量: 244 MB, 最大: 3,904 MB
传统线程池 - 已完成任务: 997/10000
[内存使用] 任务执行中 - 已用: 12 MB, 总量: 244 MB, 最大: 3,904 MB
传统线程池 - 已完成任务: 2,010/10000
[内存使用] 任务执行中 - 已用: 12 MB, 总量: 244 MB, 最大: 3,904 MB
传统线程池 - 已完成任务: 2,997/10000
[内存使用] 任务执行中 - 已用: 12 MB, 总量: 244 MB, 最大: 3,904 MB
传统线程池 - 已完成任务: 4,000/10000
[内存使用] 任务执行中 - 已用: 12 MB, 总量: 244 MB, 最大: 3,904 MB
传统线程池 - 已完成任务: 4,994/10000
[内存使用] 任务执行中 - 已用: 12 MB, 总量: 244 MB, 最大: 3,904 MB
传统线程池 - 已完成任务: 6,015/10000
[内存使用] 任务执行中 - 已用: 12 MB, 总量: 244 MB, 最大: 3,904 MB
传统线程池 - 已完成任务: 7,002/10000
[内存使用] 任务执行中 - 已用: 12 MB, 总量: 244 MB, 最大: 3,904 MB
传统线程池 - 已完成任务: 8,012/10000
[内存使用] 任务执行中 - 已用: 12 MB, 总量: 244 MB, 最大: 3,904 MB
传统线程池 - 已完成任务: 9,006/10000
[内存使用] 任务执行中 - 已用: 12 MB, 总量: 244 MB, 最大: 3,904 MB
传统线程池 - 所有任务完成
[内存使用] 任务完成后 - 已用: 12 MB, 总量: 244 MB, 最大: 3,904 MB
总耗时: 69,092 毫秒
平均每个任务耗时: 6.91 毫秒
===== 测试虚拟线程 =====
[内存使用] 虚拟线程执行前 - 已用: 12 MB, 总量: 244 MB, 最大: 3,904 MB
[内存使用] 虚拟线程执行器创建后 - 已用: 12 MB, 总量: 244 MB, 最大: 3,904 MB
虚拟线程 - 已完成任务: 7/10000 (线程: VirtualThread[#46]/runnable@ForkJoinPool-1-worker-2)
[内存使用] 虚拟任务执行中 - 已用: 24 MB, 总量: 244 MB, 最大: 3,904 MB
虚拟线程 - 已完成任务: 1,000/10000 (线程: VirtualThread[#1063]/runnable@ForkJoinPool-1-worker-6)
[内存使用] 虚拟任务执行中 - 已用: 24 MB, 总量: 244 MB, 最大: 3,904 MB
虚拟线程 - 已完成任务: 2,001/10000 (线程: VirtualThread[#2063]/runnable@ForkJoinPool-1-worker-7)
[内存使用] 虚拟任务执行中 - 已用: 24 MB, 总量: 244 MB, 最大: 3,904 MB
虚拟线程 - 已完成任务: 2,998/10000 (线程: VirtualThread[#3063]/runnable@ForkJoinPool-1-worker-2)
[内存使用] 虚拟任务执行中 - 已用: 24 MB, 总量: 244 MB, 最大: 3,904 MB
虚拟线程 - 已完成任务: 3,998/10000 (线程: VirtualThread[#4070]/runnable@ForkJoinPool-1-worker-15)
[内存使用] 虚拟任务执行中 - 已用: 24 MB, 总量: 244 MB, 最大: 3,904 MB
虚拟线程 - 已完成任务: 5,000/10000 (线程: VirtualThread[#5070]/runnable@ForkJoinPool-1-worker-5)
[内存使用] 虚拟任务执行中 - 已用: 24 MB, 总量: 244 MB, 最大: 3,904 MB
虚拟线程 - 已完成任务: 5,999/10000 (线程: VirtualThread[#6070]/runnable@ForkJoinPool-1-worker-10)
[内存使用] 虚拟任务执行中 - 已用: 24 MB, 总量: 244 MB, 最大: 3,904 MB
虚拟线程 - 已完成任务: 7,001/10000 (线程: VirtualThread[#7070]/runnable@ForkJoinPool-1-worker-15)
[内存使用] 虚拟任务执行中 - 已用: 24 MB, 总量: 244 MB, 最大: 3,904 MB
虚拟线程 - 已完成任务: 8,000/10000 (线程: VirtualThread[#8070]/runnable@ForkJoinPool-1-worker-12)
[内存使用] 虚拟任务执行中 - 已用: 24 MB, 总量: 244 MB, 最大: 3,904 MB
虚拟线程 - 已完成任务: 8,999/10000 (线程: VirtualThread[#9070]/runnable@ForkJoinPool-1-worker-13)
[内存使用] 虚拟任务执行中 - 已用: 24 MB, 总量: 244 MB, 最大: 3,904 MB
虚拟线程 - 所有任务完成
[内存使用] 虚拟任务完成后 - 已用: 24 MB, 总量: 244 MB, 最大: 3,904 MB
总耗时: 327 毫秒
平均每个任务耗时: 0.03 毫秒
虚拟线程示例信息:
最后一个虚拟线程: Thread[#1,main,5,main]
是否为虚拟线程: false
可以看到,16个传统线程就已经花费了12mb,而10000个虚拟线程仅占用24m,比例达到了恐怖的1:625。
虚拟线程由于其4大核心特性,具有强大的高并发处理能力,可以轻松支持百万级并发连接,特别适合现代高并发应用场景;其不仅支持现有的绝大部分生态,同时拥有JVM的自动资源管理能力,但是其在纯计算领域无法超越传统线程,还会因为调度开销导致性能下降,且其调试难度更高,且应该避免池化虚拟线程。