大翔(CTO):"小崔,咱们的网关服务又扛不住了,线程池打满,响应时间飙到 5 秒!" 小崔(后端开发):"大翔哥,我已经把线程池调到 500 了,再调下去上下文切换开销更大,而且每个平台线程占 1MB 栈内存,机器内存快撑爆了……" 白歌(架构师):"问题的根源在于 —— 我们用有限的平台线程去处理大量等待 I/O 的任务。JDK 21 的虚拟线程就是来解决这个问题的,它让 '一个请求一个线程' 的编程模型重新变得可行。" 孔蓝(后端新人):"虚拟线程?是像 Go 的 goroutine 那样的东西吗?" 白歌:"没错!虚拟线程由 JVM 调度,内存开销只有几 KB,可以轻松创建上百万个。今天我们就来彻底搞懂它。"
虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
什么是虚拟线程
| 概念 | 描述 |
|---|---|
| 虚拟线程(Virtual Thread) | 由 JVM 管理的轻量级线程,不直接绑定操作系统线程,调度在 Java 运行时完成 |
| 平台线程(Platform Thread) | 传统的 Java 线程,是操作系统线程的直接包装,由 OS 调度,创建和切换成本高 |
| 载体线程(Carrier Thread) | 执行虚拟线程的平台线程,虚拟线程在阻塞时会从载体线程上"卸载",让载体线程去执行其他虚拟线程 |
| 卸载(Unmount) | 虚拟线程遇到阻塞操作(如 I/O)时,暂时从载体线程上分离,载体线程可继续执行其他虚拟线程 |
| 钉住(Pin) | 虚拟线程因某些原因无法卸载,被迫继续占用载体线程,失去了虚拟线程的核心优势 |
虚拟线程 vs 平台线程:本质区别
| 对比维度 | 平台线程 | 虚拟线程 |
|---|---|---|
| 内存占用 | 默认 ~1 MB(栈空间) | 初始仅 ~200-300 bytes,栈在 Java 堆上 |
| 调度者 | 操作系统内核 | JVM(协作调度) |
| 创建成本 | 高(系统调用) | 极低(纯 Java 操作) |
| 上下文切换 | 内核态切换,昂贵 | JVM 用户态切换,廉价 |
| 适用数量级 | 数百到数千 | 数百万 |
| 阻塞代价 | 占用整个 OS 线程 | 自动卸载,释放载体线程 |
核心机制:卸载(Unmount)与挂载(Mount)
关键理解:虚拟线程的阻塞发生在 Java 运行时层面,不会阻塞底层的操作系统线程。JVM 在网络 I/O、文件系统 I/O、
LockSupport.park()等场景中会自动触发卸载。
基础用法
方式一:Thread.startVirtualThread()
// === 场景说明:最简洁的虚拟线程创建方式,适合一次性任务 ===
// 创建并启动一个虚拟线程
Thread vThread = Thread.startVirtualThread(() -> {
System.out.println("当前线程: " + Thread.currentThread()); // 行内注释:线程名以 "VirtualThread" 开头
System.out.println("是否为虚拟线程: " + Thread.currentThread().isVirtual()); // 行内注释:返回 true
try {
Thread.sleep(1000); // 行内注释:sleep 会触发卸载,不占用载体线程
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("虚拟线程执行完毕");
});
// 等待虚拟线程结束
vThread.join();
当前线程: VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1
是否为虚拟线程: true
虚拟线程执行完毕
方式二:Thread.ofVirtual() 工厂方法
// === 场景说明:使用 Thread.Builder 工厂,支持更多配置(名称、异常处理器等) ===
// 创建带名称的虚拟线程
Thread vThread = Thread.ofVirtual()
.name("order-processing-", 0) // 行内注释:名称前缀 + 自增序号
.uncaughtExceptionHandler((t, e) ->
System.err.println("线程 " + t + " 抛出异常: " + e))
.start(() -> {
System.out.println("线程名称: " + Thread.currentThread().getName());
System.out.println("虚拟线程? " + Thread.currentThread().isVirtual());
});
vThread.join();
线程名称: order-processing-0
虚拟线程? true
方式三:Executors.newVirtualThreadPerTaskExecutor()
// === 场景说明:为每个任务创建一个虚拟线程,无需配置线程池大小,适合替代固定大小线程池 ===
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { // 行内注释:实现 AutoCloseable,用完需关闭
// 提交 5 个任务,每个任务运行在独立的虚拟线程中
for (int i = 0; i < 5; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("任务 " + taskId + " 运行在: " + Thread.currentThread());
try {
Thread.sleep(500); // 模拟 I/O 等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return taskId;
});
}
} // 行内注释:try-with-resources 自动关闭,等待所有任务完成
任务 0 运行在: VirtualThread[#22]/runnable@ForkJoinPool-1-worker-1
任务 1 运行在: VirtualThread[#23]/runnable@ForkJoinPool-1-worker-2
任务 2 运行在: VirtualThread[#24]/runnable@ForkJoinPool-1-worker-3
任务 3 运行在: VirtualThread[#25]/runnable@ForkJoinPool-1-worker-1
任务 4 运行在: VirtualThread[#26]/runnable@ForkJoinPool-1-worker-2
钉住(Pin)问题
虚拟线程在以下场景中会被"钉住",无法卸载,导致载体线程被占用:
| 钉住原因 | 说明 | 解决方案 |
|---|---|---|
synchronized 同步块/方法 | 虚拟线程在 synchronized 内遇到阻塞时无法卸载 | 改用 ReentrantLock(支持虚拟线程卸载) |
Object.wait() | 调用 wait() 时会钉住载体线程 | 改用 Condition.await() 或 LockSupport.park() |
| JNI 调用 | 进入本地代码时无法卸载 | 避免在高并发路径中使用 JNI |
压测对比:1 万个线程
// === 场景说明:对比创建 10000 个平台线程 vs 10000 个虚拟线程的资源消耗 ===
// 运行前请用 -Xmx 调整堆内存,平台线程版本需要较大内存
import java.util.concurrent.*;
public class ThreadStressTest {
// ❌ 平台线程版本:创建 10000 个平台线程,极易 OOM 或耗时极长
static void platformThreadTest() throws InterruptedException {
long start = System.currentTimeMillis();
int count = 10000;
Thread[] threads = new Thread[count];
for (int i = 0; i < count; i++) {
threads[i] = new Thread(() -> {
try {
Thread.sleep(1000); // 模拟 I/O 等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
threads[i].start(); // 行内注释:每个 start() 都向 OS 申请线程,成本高
}
for (Thread t : threads) {
t.join();
}
System.out.println("平台线程耗时: " + (System.currentTimeMillis() - start) + "ms");
}
// ✅ 虚拟线程版本:创建 10000 个虚拟线程,内存占用极低
static void virtualThreadTest() throws InterruptedException {
long start = System.currentTimeMillis();
int count = 10000;
Thread[] threads = new Thread[count];
for (int i = 0; i < count; i++) {
threads[i] = Thread.startVirtualThread(() -> {
try {
Thread.sleep(1000); // 行内注释:sleep 触发卸载,不占用载体线程
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 行内注释:创建虚拟线程是纯 Java 操作,极快
}
for (Thread t : threads) {
t.join();
}
System.out.println("虚拟线程耗时: " + (System.currentTimeMillis() - start) + "ms");
}
public static void main(String[] args) throws Exception {
System.out.println("=== 虚拟线程压测对比 ===");
System.out.println("可用处理器: " + Runtime.getRuntime().availableProcessors());
virtualThreadTest(); // 推荐先运行虚拟线程版本
// platformThreadTest(); // 谨慎运行!可能需要调大内存
}
}
=== 虚拟线程压测对比 ===
可用处理器: 8
虚拟线程耗时: 1256ms
(平台线程版本在默认 JVM 配置下通常会抛出:
java.lang.OutOfMemoryError: unable to create native thread
)
实战场景:飞翔科技网关层改造
// === 场景说明:模拟网关层改造,对比线程池模型与虚拟线程模型的吞吐量 ===
import java.util.concurrent.*;
import java.util.stream.IntStream;
public class GatewayRefactorDemo {
// 模拟 I/O 操作(如数据库查询、HTTP 调用)
static void simulateIO(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 改造前:固定大小平台线程池
static void oldStyleGateway(int requestCount) throws Exception {
long start = System.currentTimeMillis();
try (var executor = Executors.newFixedThreadPool(200)) { // 行内注释:最多 200 并发
var futures = IntStream.range(0, requestCount)
.mapToObj(i -> executor.submit(() -> simulateIO(100)))
.toList();
for (var f : futures) f.get();
}
System.out.println("改造前(平台线程池200) 处理 " + requestCount + " 请求耗时: "
+ (System.currentTimeMillis() - start) + "ms");
}
// 改造后:虚拟线程执行器
static void newStyleGateway(int requestCount) throws Exception {
long start = System.currentTimeMillis();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { // 行内注释:无上限,每个任务一个虚拟线程
var futures = IntStream.range(0, requestCount)
.mapToObj(i -> executor.submit(() -> simulateIO(100)))
.toList();
for (var f : futures) f.get();
}
System.out.println("改造后(虚拟线程) 处理 " + requestCount + " 请求耗时: "
+ (System.currentTimeMillis() - start) + "ms");
}
public static void main(String[] args) throws Exception {
int requests = 5000;
System.out.println("=== 飞翔科技网关压测(每个请求模拟 100ms I/O)===");
newStyleGateway(requests); // 先跑虚拟线程版本
// oldStyleGateway(requests); // 平台线程池版(200 并发处理 5000 请求,至少需要 2500ms 以上)
}
}
=== 飞翔科技网关压测(每个请求模拟 100ms I/O)===
改造后(虚拟线程) 处理 5000 请求耗时: 1347ms
(改造前使用 200 线程池处理 5000 个 100ms I/O 的请求,
理论最短时间 = 5000 / 200 * 100ms = 2500ms,
实际因上下文切换会更长)
适用场景与不适用场景
| 类型 | 场景 | 原因 |
|---|---|---|
| ✅ 适用 | 高并发 I/O 密集型服务 | 虚拟线程在等待 I/O 时自动卸载,载体线程利用率高 |
| ✅ 适用 | 每个请求一个线程的 Web 框架 | 编程模型简单,无需手动管理异步回调 |
| ❌ 不适用 | 计算密集型任务 | 虚拟线程不增加 CPU 并行度,纯计算不会触发卸载 |
| ❌ 不适用 | 长时间持有 synchronized 锁 | 钉住载体线程,丧失虚拟线程优势 |
| ❌ 不适用 | 大量 JNI 调用 | JNI 调用期间无法卸载 |
易错场景
错误一:在虚拟线程中使用 synchronized
// ❌ 错误:在 synchronized 块内做 I/O,会钉住载体线程
public class PinnedExample {
private static final Object LOCK = new Object();
public static void wrongWay() {
for (int i = 0; i < 100; i++) {
Thread.startVirtualThread(() -> {
synchronized (LOCK) { // 行内注释:钉住!虚拟线程在 synchronized 内阻塞时无法卸载
System.out.println("线程: " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // I/O 等待,但载体线程被钉住!
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
}
// ✅ 正确:改用 ReentrantLock
private static final ReentrantLock LOCK2 = new ReentrantLock();
public static void correctWay() {
for (int i = 0; i < 100; i++) {
Thread.startVirtualThread(() -> {
LOCK2.lock(); // 行内注释:获取锁,但等待锁时仍可卸载(公平锁除外)
try {
System.out.println("线程: " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 行内注释:✅ 可卸载!载体线程被释放
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} finally {
LOCK2.unlock();
}
});
}
}
}
错误二:用虚拟线程跑 CPU 密集型任务
// ❌ 错误:虚拟线程不适合纯计算任务
public class CpuIntensiveWrong {
public static void main(String[] args") {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
// 行内注释:纯计算,永不触发卸载,虚拟线程数量远超 CPU 核心数,反而增加调度开销
long sum = 0;
for (long j = 0; j < 1_000_000_0; j++) {
sum += j;
}
return sum;
});
}
}
}
}
// ✅ 正确:CPU 密集型任务使用平台线程池,线程数 = CPU 核心数
public class CpuIntensiveCorrect {
public static void main(String[] args") {
int cores = Runtime.getRuntime().availableProcessors();
try (var executor = Executors.newFixedThreadPool(cores)) { // 行内注释:计算任务线程数 = CPU 核心数
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
long sum = 0;
for (long j = 0; j < 1_000_000_0; j++) {
sum += j;
}
return sum;
});
}
}
}
}
错误三:误以为虚拟线程能突破 CPU 上限
// ❌ 错误认知:创建 1 万个虚拟线程 = 1 万个并发计算
// 实际情况:虚拟线程只是调度单位,底层载体线程(ForkJoinPool)大小 = CPU 核心数
// 对于计算任务,1 万个虚拟线程反而比 8 个平台线程更慢(调度开销)
// ✅ 正确理解:
// - 虚拟线程提升的是 "等待 I/O 时的吞吐量",不是 CPU 计算速度
// - 计算任务并发度上限仍然是 CPU 核心数
面试考点
问题 1:虚拟线程和平台线程的根本区别是什么?虚拟线程是如何实现"一个请求一个线程"而不耗尽系统资源的?
虚拟线程与平台线程的根本区别在于调度层级和内存模型。平台线程是 OS 线程的一对一映射,由操作系统调度,每个线程占用约 1MB 栈内存;虚拟线程由 JVM 调度,栈帧存储在 Java 堆上,初始内存开销仅几百字节。虚拟线程通过"卸载"机制实现高并发:当虚拟线程执行阻塞 I/O 时,JVM 将其从载体线程(一个普通的平台线程)上卸载,载体线程可继续执行其他虚拟线程。这样,少量载体线程(默认等于 CPU 核心数)就能支撑海量虚拟线程并发,使得"一个请求一个线程"的编程模型在高并发场景下重新变得可行。
问题 2:什么是虚拟线程的"钉住"(Pin)问题?哪些场景会导致钉住?如何避免?
钉住是指虚拟线程在无法卸载的情况下占有载体线程,导致载体线程在虚拟线程阻塞期间无法执行其他虚拟线程。主要钉住场景包括:1)在
synchronized同步块/方法内遇到阻塞操作;2)调用Object.wait();3)执行 JNI 本地方法。避免方案:将synchronized替换为ReentrantLock(ReentrantLock的lock()在等待时也会钉住,但Condition.await()不会);避免在虚拟线程中长时间持有任何锁;减少 JNI 调用。需要注意的是,JDK 21 中部分synchronized钉住问题已在后续版本中通过 JEP 453(Structured Concurrency)等改进,但最佳实践仍是优先使用java.util.concurrent.locks包中的锁。
问题 3:Executors.newVirtualThreadPerTaskExecutor() 没有线程池大小限制,会不会导致资源耗尽?如何正确使用?
虽然
newVirtualThreadPerTaskExecutor()不限制虚拟线程数量,但并不意味着没有资源上限。限制来自于:1)外部资源,如数据库连接池大小、HTTP 连接池大小、下游服务吞吐能力;2)内存,虽然单个虚拟线程内存小,但海量虚拟线程的局部变量和调用栈累积仍可能 OOM;3)载体线程的 ForkJoinPool 饱和(但 I/O 型任务不会饱和)。正确使用方式:在虚拟线程之上,对外部资源访问处进行限流(如使用Semaphore限制并发数据库查询数),而不是直接限制虚拟线程数量。虚拟线程的设计哲学是"不限制数量,而是限制它们访问的共享资源"。
知识图谱
白歌(架构师):"虚拟线程不是银弹,它解决的是 '等待' 的成本,而不是 '计算' 的速度。理解卸载和钉住,是用好虚拟线程的关键。" 小崔(后端开发):"明白了!我们网关的瓶颈在 I/O 等待,换成虚拟线程后,同样的硬件配置能撑住 10 倍的并发量。" 孔蓝(后端新人):"那我以后是不是可以随便
newVirtualThreadPerTaskExecutor了?" 大翔(CTO):"可以,但记得 —— 虚拟线程免费,数据库连接不免费。限流要在资源层做,不要在线程层做。去写代码吧!"