Java 21正式引入了虚拟线程(Virtual Threads),这是Project Loom项目的核心成果。传统Java应用中,每个请求占用一个平台线程,当并发量飙升时,线程池迅速耗尽,系统响应急剧下降。虚拟线程通过将线程的调度从操作系统内核转移到JVM层面,用极低的内存开销实现了百万级并发连接,彻底改变了Java高并发编程的范式。
本文将通过实际代码示例,带你从零掌握Virtual Threads的核心概念、使用方式、常见陷阱以及性能调优技巧。
一、什么是虚拟线程
Java中的线程一直是对操作系统内核线程的一对一映射。每个平台线程(Platform Thread)默认占用约1MB的栈内存,这意味着一台16GB内存的机器最多只能创建几千个线程。虚拟线程则是由JVM自行调度的轻量级线程,初始栈内存仅几百字节,可以轻松创建数百万个而不耗尽内存。
虚拟线程的关键特性:
- 极轻量:初始内存占用仅几百字节,按需增长
- JVM调度:由ForkJoinPool调度,不依赖操作系统线程调度器
- 挂载/卸载:当虚拟线程执行阻塞操作(I/O、sleep等)时,会自动从载体线程上卸载,释放载体给其他虚拟线程使用
二、创建虚拟线程的几种方式
Java 21提供了多种创建虚拟线程的方式,从最简单的Thread.ofVirtual()到结构化并发,覆盖不同场景。
import java.util.concurrent.*;
public class VirtualThreadDemo {
public static void main(String[] args) throws Exception {
// 方式1:直接创建并启动虚拟线程
Thread vt = Thread.ofVirtual().name("my-vt").start(() -> {
System.out.println("Running in: " + Thread.currentThread());
});
vt.join();
// 方式2:使用虚拟线程ExecutorService(推荐)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
System.out.println("Task 1 done");
});
}
// 方式3:批量创建10万个虚拟线程
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
final int taskId = i;
executor.submit(() -> {
Thread.sleep(Duration.ofMillis(100));
return "Task " + taskId;
});
}
}
}
}

三、虚拟线程 vs 平台线程:性能对比
下面通过一个模拟I/O密集型任务的benchmark,直观对比虚拟线程和传统线程池的表现。
import java.time.Duration;
import java.util.concurrent.*;
public class BenchmarkDemo {
static String ioTask(int id) throws Exception {
Thread.sleep(Duration.ofMillis(200));
return "Result-" + id;
}
public static void main(String[] args) throws Exception {
int taskCount = 10_000;
// 测试1:传统固定线程池(200线程)
long start = System.currentTimeMillis();
try (var pool = Executors.newFixedThreadPool(200)) {
var futures = new ArrayList>();
for (int i = 0; i < taskCount; i++) {
final int id = i;
futures.add(pool.submit(() -> ioTask(id)));
}
for (var f : futures) f.get();
}
long platformTime = System.currentTimeMillis() - start;
System.out.println("Platform Threads: " + platformTime + "ms");
// 测试2:虚拟线程
start = System.currentTimeMillis();
try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = new ArrayList>();
for (int i = 0; i < taskCount; i++) {
final int id = i;
futures.add(pool.submit(() -> ioTask(id)));
}
for (var f : futures) f.get();
}
long virtualTime = System.currentTimeMillis() - start;
System.out.println("Virtual Threads: " + virtualTime + "ms");
}
}
在典型的I/O密集型场景中,虚拟线程的吞吐量通常是200线程池的5-10倍,因为10000个任务可以几乎同时开始sleep,而不是排队等待线程。
四、常见陷阱:Pinning问题
虚拟线程并非万能药。当虚拟线程在synchronized块内执行阻塞操作时,会发生线程钉住(Pinning),即虚拟线程无法从载体线程卸载,导致载体线程被长时间占用。
import java.util.concurrent.locks.ReentrantLock;
import java.time.Duration;
public class PinningExample {
// synchronized + 阻塞 = pinning (应避免)
private static final Object lock = new Object();
static void badExample() throws Exception {
synchronized (lock) {
Thread.sleep(Duration.ofSeconds(1)); // pinning!
}
}
// ReentrantLock + 阻塞 = 正常卸载 (推荐)
private static final ReentrantLock reentrantLock = new ReentrantLock();
static void goodExample() throws Exception {
reentrantLock.lock();
try {
Thread.sleep(Duration.ofSeconds(1)); // 正常卸载
} finally {
reentrantLock.unlock();
}
}
}
可以通过JVM参数-Djdk.tracePinnedThreads=short来检测pinning问题,开发阶段建议开启。

五、在Spring Boot中使用虚拟线程
Spring Boot 3.2+原生支持虚拟线程,只需一行配置即可让Tomcat使用虚拟线程处理请求。
# application.yml
spring:
threads:
virtual:
enabled: true
启用后,每个HTTP请求都会在一个独立的虚拟线程中处理,即使存在大量慢接口调用,也不会耗尽线程池。对于更早的Spring Boot版本,也可以手动配置Tomcat的线程池:
@Bean
TomatorProtocolHandlerCustomizer> virtualThreads() {
return handler -> handler.setExecutor(
Executors.newVirtualThreadPerTaskExecutor()
);
}
总结
Java虚拟线程是近年来Java平台最重要的特性之一,核心要点如下:
- 适用场景:I/O密集型任务(网络请求、文件读写、数据库查询),而非CPU密集型计算
- 创建方式:推荐使用
Executors.newVirtualThreadPerTaskExecutor(),每个任务一个虚拟线程 - 避免pinning:用
ReentrantLock替代synchronized,避免在锁内执行阻塞操作 - 框架支持:Spring Boot 3.2+、Quarkus 3.x、Helidon 4.x均已原生支持
- 监控手段:使用
-Djdk.tracePinnedThreads=short检测pinning,JFR事件监控虚拟线程生命周期
虚拟线程让Java开发者可以用最简单的同步代码风格,获得接近异步编程的高并发性能,是构建高吞吐后端服务的利器。
汤不热吧