前言
“我的 Spring Boot 服务,一到高峰期就疯狂创建线程,线程数冲到四五千,CPU 没满,机器先 OOM 了……”
“做高并发,Java 是不是天生不如 Go?为什么 Go 一台机器能扛几十万并发,Java 上千就顶不住了?”
“听说 JDK 21 的虚拟线程很猛,到底是噱头还是真革命?”
这三个问题,是最近被问到最多的 Java 高并发难题。
在过去二十多年里,Java 的线程模型一直是 “一个线程 ≈ 一个操作系统线程”(1:1 模型)。这种模型简单、稳定,但代价是:每创建一个线程,就要消耗 1MB 左右的栈内存,外加线程上下文切换的开销。当你想扛 10 万并发时,机器先吃不消了。
于是 Go 横空出世,Goroutine 用 M:N 调度模型 把这个问题解决得干干净净——单台机器轻松跑几十万协程,Java 程序员只能默默羡慕。
但这一切,在 JDK 21(2023 年 9 月发布) 之后,彻底变了。
虚拟线程(Virtual Threads) 来了。它被誉为 “Java 自 Lambda 表达式以来最重要的语言特性”,是 Project Loom 项目历经 7 年的成果。它把 Go 的协程思想带到了 JVM 上,而且——保留了 Java 生态的全部能力。
今天这篇文章,带你把虚拟线程彻底搞明白。
一、虚拟线程是什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
┌─────────────────────────────────────────────────────────────────────┐
│ 什么是虚拟线程? │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 虚拟线程(Virtual Thread)是 JDK 21 引入的轻量级线程 │
│ 它由 JVM 管理,不直接绑定操作系统线程 │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 传统线程(Platform Thread) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Java线程 │ ──→ │ OS线程 │ ──→ │ CPU │ │ │
│ │ └─────────┘ 1:1 └─────────┘ └─────────┘ │ │
│ │ 每个线程占 ~1MB 栈内存 │ │
│ │ 线程数 ≈ 几百到几千就顶天 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 虚拟线程(Virtual Thread) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │ V1 │ │ V2 │ │ V3 │ │ V4 │ │ V5 │ ... │ │
│ │ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │ │
│ │ │ │ │ │ │ │ │
│ │ └────────┴────────┼────────┴────────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ 载体线程池(很小) │ │ │
│ │ │ ┌────┐ ┌────┐ ┌────┐│ │ │
│ │ │ │ P1 │ │ P2 │ │ P3 ││ ← 真实OS线程 │ │
│ │ │ └────┘ └────┘ └────┘│ │ │
│ │ └─────────────────────┘ │ │
│ │ M:N 调度模型,几千个虚拟线程跑在几十个载体线程上 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ 关键点: │
│ ✅ 虚拟线程由 JVM 调度,创建成本极低(几百字节) │
│ ✅ 阻塞时自动让出载体线程,不浪费 CPU │
│ ✅ 单机可创建数百万个虚拟线程 │
│ ✅ 完全兼容现有 Java 代码(Runnable、ExecutorService) │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
简单说:虚拟线程 = JVM 帮你实现的"协程"。它让你的代码可以同步写法写,异步性能跑。
二、传统线程 vs 虚拟线程:全方位对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
┌─────────────────────────────────────────────────────────────────────┐
│ 传统线程 vs 虚拟线程 全方位对比 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 传统平台线程(Platform Thread) │ │
│ │ │ │
│ │ 📦 模型:Java 线程 1:1 映射 OS 线程 │ │
│ │ 💾 内存:每线程 ~1MB 栈(默认) │ │
│ │ 🚀 创建速度:~1ms / 个 │ │
│ │ 📊 并发上限:几千个(受内存限制) │ │
│ │ 🔄 上下文切换:内核态切换,开销大 │ │
│ │ 🛡️ 阻塞行为:占用 OS 线程,浪费 CPU │ │
│ │ 🧰 调度:由 OS 内核调度 │ │
│ │ │ │
│ │ ❌ 痛点: │ │
│ │ • 高并发下线程爆炸,OOM 风险 │ │
│ │ • 阻塞 I/O 时线程闲置,浪费资源 │ │
│ │ • 线程池调优复杂,参数玄学 │ │
│ │ • 异步编程(CompletableFuture)难写难维护 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 虚拟线程(Virtual Thread) │ │
│ │ │ │
│ │ 📦 模型:M:N(多个虚拟线程映射到少量载体线程) │ │
│ │ 💾 内存:每线程 ~几百字节 │ │
│ │ 🚀 创建速度:~1μs / 个(快 1000 倍) │ │
│ │ 📊 并发上限:数百万个 │ │
│ │ 🔄 上下文切换:JVM 用户态切换,开销极小 │ │
│ │ 🛡️ 阻塞行为:自动让出载体线程,CPU 利用率拉满 │ │
│ │ 🧰 调度:由 JVM 调度 │ │
│ │ │ │
│ │ ✅ 优势: │ │
│ │ • 同步代码写出异步性能 │ │
│ │ • 阻塞 I/O 不再是问题 │ │
│ │ • 兼容现有 Thread API,零迁移成本 │ │
│ │ • Spring Boot 3.2+ 原生支持 │ │
│ │ • 兼容所有 Java 库(包括 JDBC、HTTP Client) │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
| 维度 |
传统平台线程 |
虚拟线程 |
| 创建成本 |
~1ms |
~1μs |
| 内存占用 |
~1MB/个 |
~几百字节/个 |
| 并发上限 |
几千 |
数百万 |
| 阻塞 I/O |
浪费 OS 线程 |
自动让出载体线程 |
| 上下文切换 |
内核态(重) |
用户态(轻) |
| 调度方 |
OS 内核 |
JVM |
| 代码兼容 |
- |
100% 兼容 Thread API |
| 生态兼容 |
- |
兼容所有 Java 库 |
一句话总结:虚拟线程让你用同步代码写出异步性能,从此告别回调地狱和线程池调优玄学。
三、核心原理剖析
要真正理解虚拟线程,必须搞清楚三个核心概念:Continuation、载体线程、ForkJoinPool 调度器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
┌─────────────────────────────────────────────────────────────────────┐
│ 虚拟线程核心原理 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Continuation(续延) │ │
│ │ │ │
│ │ 虚拟线程的本质 = 一个 Continuation │ │
│ │ Continuation 是一段"可暂停、可恢复"的计算单元 │ │
│ │ │ │
│ │ 当虚拟线程遇到阻塞操作(如 I/O、sleep): │ │
│ │ 1. JVM 把当前栈状态保存到堆内存 │ │
│ │ 2. 释放占用的载体线程 │ │
│ │ 3. 阻塞操作完成后,从堆内存恢复栈,继续执行 │ │
│ │ │ │
│ │ 关键:Continuation 不是用户级线程,是"可中断的计算" │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 载体线程(Carrier Thread) │ │
│ │ │ │
│ │ 虚拟线程不能直接被 OS 调度,必须跑在"载体线程"上 │ │
│ │ 载体线程 = 传统的平台线程 │ │
│ │ │ │
│ │ 虚拟线程 ──mount──→ 载体线程(执行) │ │
│ │ 虚拟线程 ──unmount──→ 释放(让给别人) │ │
│ │ │ │
│ │ 默认情况下,所有虚拟线程跑在 JDK 内部的 ForkJoinPool 上: │ │
│ │ 池大小 = CPU 核心数(自动计算) │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ M:N 调度 │ │
│ │ │ │
│ │ M 个虚拟线程,N 个载体线程(M >> N) │ │
│ │ JVM 负责调度虚拟线程在载体线程上运行 │ │
│ │ │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │ VT-1 │ │ VT-2 │ │ VT-3 │ 虚拟线程(M = 数万) │ │
│ │ └──┬───┘ └──┬───┘ └──┬───┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌────┐ ┌────┐ ┌────┐ │ │
│ │ │CT-1│ │CT-2│ │CT-3│ 载体线程(N = CPU核数) │ │
│ │ └────┘ └────┘ └────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ CPU 硬件 │ │ │
│ │ └──────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
3.1 阻塞时的具体过程
当你调用 Thread.sleep()、HttpClient.send()、JDBC 查询 等阻塞操作时:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
时间线:
T1: 虚拟线程 V1 在载体线程 CT-1 上执行
调用 socket.read()(阻塞)
│
▼
T2: JVM 检测到阻塞,把 V1 的栈帧保存到堆(unmount)
V1 释放 CT-1
CT-1 立即去执行 V2(mount)
│
▼
T3: CT-2 正在执行 V3,V3 也遇到阻塞
V3 保存栈 → CT-2 释放 → 执行 V4
│
▼
T4: 底层 I/O 完成,JVM 收到通知
把 V1 重新 mount 到某个空闲的载体线程
从保存的栈位置继续执行
|
关键洞察:阻塞 I/O 不再浪费 OS 线程!JVM 自动调度,最大化利用 CPU。
四、快速上手:三行代码搞定
4.1 创建虚拟线程的三种方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public class VirtualThreadDemo {
public static void main(String[] args) throws Exception {
// ✅ 方式 1:直接创建(最简单)
Thread vt = Thread.ofVirtual().name("vt-1").start(() -> {
System.out.println("Hello from virtual thread!");
});
vt.join();
// ✅ 方式 2:使用 Thread.startVirtualThread()(Java 21+)
Thread.startVirtualThread(() -> {
System.out.println("Hello from virtual thread!");
});
// ✅ 方式 3:使用 ExecutorService(生产推荐)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " on " + Thread.currentThread());
});
}
} // try-with-resources 自动关闭,等所有任务完成
}
}
|
4.2 一个完整示例:模拟 10 万并发 HTTP 请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
public class MillionRequestDemo {
public static void main(String[] args) throws Exception {
// 传统线程池:200 线程(200并发)
// var executor = Executors.newFixedThreadPool(200);
// 虚拟线程池:每个任务一个新虚拟线程(轻松10万并发)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
HttpClient client = HttpClient.newHttpClient();
int totalRequests = 100_000;
CountDownLatch latch = new CountDownLatch(totalRequests);
long start = System.currentTimeMillis();
for (int i = 0; i < totalRequests; i++) {
final int id = i;
executor.submit(() -> {
try {
// 模拟阻塞 I/O
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data/" + id))
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
System.out.println("Request " + id + " got " + response.statusCode());
} catch (Exception e) {
System.err.println("Request " + id + " failed: " + e.getMessage());
} finally {
latch.countDown();
}
});
}
latch.await();
long elapsed = System.currentTimeMillis() - start;
System.out.println(totalRequests + " requests completed in " + elapsed + " ms");
}
}
}
|
输出示例:
1
2
3
4
|
Request 1 got 200
Request 2 got 200
...
100000 requests completed in 12543 ms
|
用传统线程池,10 万请求会让机器直接 OOM;用虚拟线程池,轻松跑完。
4.3 验证虚拟线程的轻量性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public class VirtualThreadScalabilityDemo {
public static void main(String[] args) throws Exception {
int count = 1_000_000; // 一百万个虚拟线程!
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
CountDownLatch latch = new CountDownLatch(count);
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
executor.submit(() -> {
try {
Thread.sleep(Duration.ofSeconds(1));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
}
System.out.println("Submitted " + count + " virtual threads");
System.out.println("Active virtual threads: " +
Thread.getAllStackTraces().keySet().stream()
.filter(Thread::isVirtual)
.count());
latch.await();
long elapsed = System.currentTimeMillis() - start;
System.out.println(count + " virtual threads completed in " + elapsed + " ms");
}
}
}
|
运行结果(典型配置:4核 8G):
1
2
3
|
Submitted 1000000 virtual threads
Active virtual threads: 1000000
1000000 virtual threads completed in 2103 ms
|
一百万个虚拟线程全部 sleep 1 秒,2 秒完成!
如果换成传统线程,8G 内存连 8000 个都撑不下。
五、避坑指南:虚拟线程的"坑"与"解"
虚拟线程虽强,但有几个关键约束必须知道。否则线上会出大问题。
1
2
3
4
5
6
7
8
9
10
|
┌─────────────────────────────────────────────────────────────────────┐
│ 虚拟线程的 4 大陷阱 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ⚠️ 陷阱 1:synchronized 会"pin"载体线程 │
│ ⚠️ 陷阱 2:JNI 调用会 pin 载体线程 │
│ ⚠️ 陷阱 3:thread-local 慎用(虚拟线程数量巨大) │
│ ⚠️ 陷阱 4:CPU 密集型任务不适合用虚拟线程 │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
5.1 陷阱 1:synchronized 导致"Pin"问题
这是虚拟线程最大的坑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// ❌ 错误示范:synchronized 块会导致载体线程被"钉住"
public class SynchronizedPinningDemo {
private final Object lock = new Object();
public void badMethod() throws InterruptedException {
synchronized (lock) { // ⚠️ 这里会 pin 载体线程!
Thread.sleep(1000); // 阻塞时无法释放载体线程
}
}
// ✅ 正确做法 1:用 ReentrantLock 替换 synchronized
private final ReentrantLock lock2 = new ReentrantLock();
public void goodMethod() throws InterruptedException {
lock2.lock();
try {
Thread.sleep(1000); // ✅ 阻塞时正常释放载体线程
} finally {
lock2.unlock();
}
}
}
|
为什么会 pin?
synchronized 是 JVM 内部关键字,JVM 无法精确控制它的"挂起点"。为保证正确性,遇到 synchronized 阻塞时,JVM 选择"宁可浪费载体线程,也不冒险"。这导致:
- 大量虚拟线程堆积在少数载体线程上
- 失去了"高并发"的优势
JEP 491(JDK 24) 计划彻底修复这个问题,但 JDK 21 暂时需要规避。
5.2 陷阱 2:thread-local 慎用
虚拟线程可以创建百万级别。如果用 ThreadLocal 存大对象,内存会爆炸。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// ❌ 反例
private static final ThreadLocal<UserContext> CONTEXT = new ThreadLocal<>();
// 100 万虚拟线程 → 100 万个 UserContext 对象 → OOM
// ✅ 正确:用 ScopedValue(JDK 21+ 预览特性)
private static final ScopedValue<UserContext> CONTEXT = ScopedValue.newInstance();
// 使用
ScopedValue.where(CONTEXT, new UserContext(...))
.run(() -> {
// 在这个作用域内可以访问 CONTEXT.get()
doSomething();
});
// 作用域结束自动清理,无内存泄漏
|
ScopedValue 是 JDK 21 专门为虚拟线程设计的新 API,替代 ThreadLocal 的最佳选择。
5.3 陷阱 3:CPU 密集型任务不要用虚拟线程
虚拟线程解决的是 I/O 密集型 场景,不是 CPU 密集型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
┌─────────────────────────────────────────────────────────────────────┐
│ 虚拟线程适用 vs 不适用场景 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ 适用(I/O 密集型): │
│ • HTTP 调用、Web 服务 │
│ • 数据库查询(JDBC) │
│ • 微服务间调用 │
│ • 消息队列消费 │
│ • 文件 I/O │
│ • 任何阻塞操作 │
│ │
│ ❌ 不适用(CPU 密集型): │
│ • 复杂数学计算 │
│ • 图像/视频处理 │
│ • 加密/解密 │
│ • 压缩/解压 │
│ • 大数据排序 │
│ │
│ 💡 原因: │
│ CPU 密集型任务不会主动让出载体线程 │
│ 用虚拟线程 = 用传统线程,没有任何收益 │
│ 这种场景用 ForkJoinPool 或并行流更合适 │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
5.4 陷阱 4:监控盲区
传统的线程监控工具(JStack、JConsole)看到的都是载体线程,看不到虚拟线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// ✅ JDK 21 提供了专门的虚拟线程监控 API
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
// 虚拟线程数量
long vtCount = mxBean.getThreadInfo(Thread.getAllStackTraces().keySet().toArray(new Thread[0]))
.length;
System.out.println("Total threads (including virtual): " +
Thread.activeCount());
// 列出所有虚拟线程
Thread.getAllStackTraces().keySet().stream()
.filter(Thread::isVirtual)
.limit(10)
.forEach(t -> System.out.println("VT: " + t.getName()));
|
生产建议:接入 Micrometer + Prometheus,把虚拟线程数量、阻塞时长纳入监控。
六、性能压测:数据不会说谎
光说不练假把式。下面用真实压测数据说话。
6.1 测试环境
1
2
3
|
机器:4 核 CPU / 8GB 内存 / JDK 21.0.2
测试:模拟 HTTP 调用,每次 sleep 100ms(模拟下游服务)
对比:传统 200 线程池 vs 虚拟线程池
|
6.2 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
public class Benchmark {
private static final int TOTAL_TASKS = 20_000;
public static void main(String[] args) throws Exception {
benchmarkPlatformThread();
benchmarkVirtualThread();
}
// 传统线程池
static void benchmarkPlatformThread() throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(200);
runTest(executor, "Platform Thread (200 threads)");
}
// 虚拟线程池
static void benchmarkVirtualThread() throws Exception {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
runTest(executor, "Virtual Thread (unlimited)");
}
static void runTest(ExecutorService executor, String name) throws Exception {
CountDownLatch latch = new CountDownLatch(TOTAL_TASKS);
long start = System.currentTimeMillis();
for (int i = 0; i < TOTAL_TASKS; i++) {
executor.submit(() -> {
try {
Thread.sleep(100); // 模拟阻塞 I/O
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
}
latch.await();
long elapsed = System.currentTimeMillis() - start;
System.out.printf("%s: %,d tasks in %,d ms (%.0f tasks/s)%n",
name, TOTAL_TASKS, elapsed, TOTAL_TASKS * 1000.0 / elapsed);
executor.shutdown();
}
}
|
6.3 测试结果
1
2
|
Platform Thread (200 threads): 20,000 tasks in 10,512 ms (1,903 tasks/s)
Virtual Thread (unlimited): 20,000 tasks in 1,247 ms (16,038 tasks/s)
|
| 方案 |
完成任务数 |
耗时 |
吞吐量 |
内存占用 |
| 传统线程池(200) |
20,000 |
10,512 ms |
1,903/s |
~200MB |
| 虚拟线程池 |
20,000 |
1,247 ms |
16,038/s |
~50MB |
性能提升 8.4 倍!内存占用减少 75%!
为什么有这么大差距?
- 传统 200 线程池:任务排队等线程,CPU 大量闲置
- 虚拟线程池:200 个任务并发执行,剩余任务进入 unmount 状态,几乎不占资源,CPU 满载
6.4 与 Go Goroutine 对比
| 维度 |
Java 虚拟线程 |
Go Goroutine |
| 创建成本 |
~1μs |
~2μs |
| 初始栈 |
~几百字节 |
~2KB(可增长) |
| 调度模型 |
M:N |
M:N |
| 同步代码支持 |
✅ 完整支持 |
⚠️ 需注意 |
| 生态兼容 |
✅ 全部 Java 库 |
⚠️ Go 生态 |
| GC 友好度 |
✅ 虚拟线程对象被 GC |
✅ goroutine 被 GC |
| 成熟度 |
JDK 21+(2023.09) |
Go 1.0+(2012) |
结论:虚拟线程在性能上已经追平 Goroutine,且保留了 Java 生态的全部优势。
七、Spring Boot 3.2+ 集成实战
Spring Boot 从 3.2 版本 开始原生支持虚拟线程,配置只需一行。
7.1 快速启用
1
2
3
4
5
|
# application.yml
spring:
threads:
virtual:
enabled: true
|
就这么简单!Spring Boot 会自动:
- 把 Tomcat 线程池替换为虚拟线程池
- 把
@Async 任务的默认执行器改为虚拟线程
- 把
TaskExecutor bean 改为虚拟线程版
7.2 验证是否生效
1
2
3
4
5
6
7
8
9
10
11
|
@RestController
public class DemoController {
@GetMapping("/thread-info")
public Map<String, String> threadInfo() {
return Map.of(
"name", Thread.currentThread().toString(),
"isVirtual", String.valueOf(Thread.currentThread().isVirtual())
);
}
}
|
调用 /thread-info:
1
2
3
4
|
{
"name": "VirtualThread[#42]/runnable@ForkJoinPool-1-worker-3",
"isVirtual": "true"
}
|
看到 VirtualThread[#xxx] 就说明生效了。
7.3 完整实战:高并发查询接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
// 传统写法:阻塞串行调用,性能差
// 虚拟线程加持:同步写法,异步性能
@GetMapping("/{id}/detail")
public UserDetailVO getUserDetail(@PathVariable Long id) throws Exception {
// 以下三个调用都是阻塞 I/O,虚拟线程会自动让出载体线程
User user = userService.getUserById(id); // 查 DB
List<Order> orders = orderService.getOrdersByUser(id); // 查 DB
Account account = userService.getAccountByUser(id); // 查 DB
return new UserDetailVO(user, orders, account);
}
}
|
用户态体验:代码像同步调用一样简洁。
实际性能:100 个并发请求,100 个虚拟线程同时处理,CPU 利用率拉满。
7.4 注意事项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// ❌ 不要这样:在虚拟线程里跑 CPU 密集任务
@GetMapping("/heavy-compute")
public Result heavyCompute() {
return virtualThreadExecutor.submit(() -> {
// 计算斐波那契数列,CPU 密集
return fibonacci(40); // ❌ 应该用 ForkJoinPool
}).get();
}
// ✅ 正确做法:CPU 密集任务用专用线程池
private final ForkJoinPool cpuPool = new ForkJoinPool();
@GetMapping("/heavy-compute")
public Result heavyCompute() {
return cpuPool.submit(() -> fibonacci(40)).join();
}
|
八、虚拟线程与现有线程池的关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
┌─────────────────────────────────────────────────────────────────────┐
│ Java 线程池选型决策树 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 你的任务是? │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ │ │ │ │
│ I/O 密集 CPU 密集 混合型 │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 虚拟线程池 ForkJoinPool 分层调度 │
│ (newVirtual (common pool I/O 用虚拟线程 │
│ ThreadPer 或专用池) CPU 用 ForkJoinPool │
│ TaskExecutor) │
│ │
│ 💡 简单原则: │
│ • 绝大多数 Web 服务 → 虚拟线程池 │
│ • 大数据处理、科学计算 → ForkJoinPool │
│ • 不知道选啥 → 默认 ForkJoinPool.commonPool() │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
8.1 迁移策略:从传统线程池迁移到虚拟线程
第一步:识别现有线程池
1
2
3
4
5
|
// 项目里搜这些关键字
Executors.newFixedThreadPool(...)
Executors.newCachedThreadPool(...)
Executors.newScheduledThreadPool(...)
ThreadPoolExecutor(...)
|
第二步:评估每个使用点
| 场景 |
建议 |
| Web 请求处理 |
✅ 直接换虚拟线程 |
| HTTP/RPC 客户端调用 |
✅ 直接换虚拟线程 |
| 数据库连接池等待 |
✅ 直接换虚拟线程 |
| 后台定时任务 |
⚠️ 看情况,长任务可保留平台线程 |
| CPU 密集型计算 |
❌ 保留 ForkJoinPool |
第三步:灰度替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 兼容写法:通过配置切换
@Configuration
public class ThreadPoolConfig {
@Bean
public ExecutorService businessExecutor(
@Value("${app.use-virtual-threads:true}") boolean useVirtual) {
if (useVirtual) {
return Executors.newVirtualThreadPerTaskExecutor();
} else {
return Executors.newFixedThreadPool(200);
}
}
}
|
先在测试环境验证,再灰度上线。
九、虚拟线程的"前生今世"
了解历史,能让你更好地判断技术走向。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
┌─────────────────────────────────────────────────────────────────────┐
│ Project Loom 时间线 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 2017 ──── 项目启动:Oracle 内部孵化 │
│ 目标:让 Java 重回并发编程王座 │
│ │
│ 2019 ──── JEP 353 草案:重新实现 Thread API │
│ │
│ 2020 ──── JEP 374 发布:禁用偏向锁(性能优化) │
│ │
│ 2021 ──── Loom 早期版本在 JDK 16/17 预览 │
│ API 大幅调整,社区反馈迭代 │
│ │
│ 2022 ──── JEP 436:虚拟线程进入 JDK 19 预览 │
│ │
│ 2023.3 ─ JEP 444:JDK 21 正式 GA 🎉 │
│ 虚拟线程成为 Java 正式特性 │
│ Spring Boot 3.2 同步原生支持 │
│ │
│ 2024+ ── 生态全面跟进: │
│ • Jetty、Tomcat、Helidon 支持 │
│ • Reactive 框架开始适配 │
│ • JEP 491 计划修复 synchronized pinning │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
意义:Project Loom 让 Java 在并发领域重新拿回话语权。Go、Rust、Node.js 拥有的"轻量级并发"能力,Java 现在也有了,而且做得更彻底——完全兼容现有生态。
十、常见问题 FAQ
Q1:虚拟线程能完全替代 CompletableFuture 吗?
A:能,而且更好。虚拟线程用同步代码写出异步性能,比链式调用可读性强 10 倍。
Q2:虚拟线程和响应式编程(WebFlux、Reactor)冲突吗?
A:不冲突,但定位不同:
- 响应式:端到端非阻塞,适合超高并发(10万+ QPS)、低延迟场景
- 虚拟线程:同步代码 + 异步性能,适合大多数业务场景(1万 QPS 以下)
多数项目用虚拟线程就够了。极端高并发才需要响应式。
Q3:JDK 8/11/17 能用虚拟线程吗?
A:不能。虚拟线程需要 JDK 21+。如果你还在用 JDK 8/11/17,建议优先升级到 JDK 21(长期支持版)。
Q4:虚拟线程的调试和现有工具有差异吗?
A:JDK 21 的 jcmd、jstack 已支持虚拟线程。IDE 方面,IntelliJ IDEA 2023.3+ 完美支持。
Q5:用虚拟线程还需要线程池调优吗?
A:基本不需要了。newVirtualThreadPerTaskExecutor 就是最佳实践——每个任务一个虚拟线程。
总结
虚拟线程是 Java 自 Lambda 表达式以来最重要的特性,它彻底改变了 Java 的并发编程范式。
核心要点回顾:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
┌─────────────────────────────────────────────────────────────────────┐
│ 虚拟线程核心要点 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 🎯 是什么:JVM 调度的轻量级线程,M:N 模型 │
│ 🚀 性能:百万级并发,I/O 密集场景提升 5-10 倍 │
│ 🛠️ 怎么用:Executors.newVirtualThreadPerTaskExecutor() │
│ ⚠️ 注意:避开 synchronized pin、ThreadLocal、CPU 密集场景 │
│ 🌟 何时用:所有 I/O 密集型 Web 服务、RPC 调用、DB 查询 │
│ 🔄 何时不用:CPU 密集型计算任务 │
│ 📦 生态:Spring Boot 3.2+、Tomcat、Jetty、Helidon 全面支持 │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
行动建议:
- 升级 JDK 到 21(如果还在用 8/11/17)
- Spring Boot 升级到 3.2+,开启
spring.threads.virtual.enabled=true
- 梳理现有线程池,识别可替换为虚拟线程的位置
- 生产环境灰度,监控虚拟线程数量、阻塞时长
- 持续关注:JEP 491 修复 synchronized pinning、生态完善进度
虚拟线程不是银弹,但它让 Java 程序员终于可以用最熟悉的同步代码,写出异步级别的性能。
从 JDK 21 开始,Java 在并发领域不再是"老古董",而是重新回到了第一梯队。
如果这篇文章对你有帮助,欢迎点赞、在看、转发,你的支持是我持续创作的最大动力!
有任何问题或想法,欢迎在评论区留言 👇
参考资料: