Java 虚拟线程 — 快速指南
看起来您只提供了来源链接,而没有贴出需要翻译的正文内容。请把要翻译的文本(包括标题、段落、列表等)粘贴在这里,我会按照要求保留源链接和原始的 Markdown 格式进行简体中文翻译。谢谢!
01 · 虚拟线程是什么
在 Project Loom 之前,Java 只有一种线程:平台线程(与操作系统内核线程 1:1 映射)。
Project Loom 引入了 虚拟线程,作为第二种轻量级线程类型。
- 平台线程 – 由操作系统调度,1:1 对应内核线程。
- 虚拟线程 – 由 JVM 在用户模式下调度,M:N 映射到内核线程。
两者都由 java.lang.Thread 表示。
关键特性
- 与平台(操作系统)线程相比,极其轻量。
- 可以安全地创建数百万个虚拟线程。
- 让开发者能够编写简单的阻塞式代码,同时保持高度可扩展性。
思维模型
| 虚拟线程 | 承载(OS)线程 |
|---|---|
| JVM 管理的轻量级线程(约 1 KB) | 真正的操作系统线程 |
| 可以创建数百万个 | 小而固定的线程池(≈ CPU 核心数) |
| 阻塞成本低且安全 | 真正稀缺的资源 |
阻塞对比
OS 线程阻塞
RestTemplate blocks an OS thread
- Thread is idle during I/O
- Under load → thread exhaustion
虚拟线程阻塞
- JVM suspends the virtual thread
- Carrier thread is released immediately
- Scales safely under high concurrency
Source: …
02 · 在 Spring Boot 中启用
一个属性。无需代码更改。
# application.yml
server:
servlet:
threads:
virtual-threads-enabled: true
要求
- Java 21+(正式版,非预览版)
- Spring Boot 3.2+
有哪些变化
- 每个 HTTP 请求都在一个全新的虚拟线程上运行。
- 控制器、服务、
RestTemplate等保持不变。
virtual-threads-enabled: true 实际做了什么
它用 每请求一个虚拟线程的执行器 替换了 Tomcat 的整个 servlet 线程池。每个进入的 HTTP 请求都会立即分配一个新的虚拟线程。没有 固定的池大小——Tomcat 不会对线程数量做限制,线程由 JVM 管理。
后果
- 整个请求生命周期(从
DispatcherServlet入口到响应写回)都在虚拟线程上执行。 - 链路中的每个阻塞调用(
RestTemplate、JDBC、Thread.sleep()等)都很轻量,因为它们已经在虚拟线程上运行。
手动卸载(Offload)方式
Tomcat 默认的 OS 线程池负责 accept 和 dispatch,然后显式地将工作交给虚拟线程执行器(例如通过 CompletableFuture.supplyAsync)。接受请求的 OS 线程会立即被释放。
Spring MVC 知道如何处理返回的 CompletableFuture:
- 挂起 servlet 处理。
- 当 future 完成时 恢复 处理。
关键点: Spring MVC 不会阻塞 servlet 线程等待 future 完成;它在内部注册回调并立即释放线程。
在这两种方式下,服务层和客户端层都保持不变。
Source: …
03 · Spring Boot 中的虚拟线程采纳策略
背景
现有的 Spring Boot 微服务:
- 使用 Spring MVC(Servlet 栈)处理传入的 HTTP 请求。
- 使用阻塞客户端(
RestTemplate、JDBC)调用多个下游服务。
约束
- 当前实现 不能更改或重写。
- 代码库中包含
synchronized块/方法,并且大量依赖ThreadLocal(SecurityContext、MDC、请求属性)。 - 服务在下游服务之间进行 I/O 密集型聚合。
- 由于线程阻塞,负载下出现可伸缩性问题。
目标: 在不破坏现有行为或引入细微运行时风险的前提下,使用 Java 虚拟线程 提升并发性和可伸缩性。
Spring Boot 提供两种方案:
- 基于属性的虚拟线程(
spring.threads.virtual.enabled=true)。 - 手动 offload 到虚拟线程执行器,使用
CompletableFuture。
方案 1:基于属性的虚拟线程(全局启用)
描述
启用
spring.threads.virtual.enabled=true
会用 每请求一个虚拟线程 的执行器替换 Tomcat 的 servlet 线程池。
每个 HTTP 请求:
- 分配一个全新的虚拟线程。
- 完全在该虚拟线程上运行(过滤器 → 控制器 → 服务 → 响应)。
- 以低成本执行阻塞调用(
RestTemplate、JDBC、Thread.sleep)。
好处
- 无需代码更改。
- 整个应用行为统一。
- 对阻塞 I/O 自动实现可伸缩性。
风险与限制
钉住风险
synchronized 块会 钉住载体线程。钉住是不可见且全局的;并发访问可能耗尽数量有限的载体线程池。
[Virtual Thread] → synchronized block ← carrier pinned → blocking I/O
如果有 N 个并发请求进入钉住的代码段,则需要 N 个载体线程。载体线程通常只有 CPU 核心数这么多,应用可能会卡死。
ThreadLocal 假设失效
- 虚拟线程生命周期短,且不会在请求之间复用。
ThreadLocal数据 不会 超出单个请求的范围。
依赖线程复用(例如在 ThreadLocal 中缓存数据)的代码可能出现异常。
缺乏控制
- 没有隔离边界——无法有选择地排除特定端点或代码路径。
- 解决问题可能需要全局重启应用,因为更改是全局性的。
方案 2:手动 offload 到虚拟线程执行器
描述
保留 Tomcat 的 OS 线程池用于接受/分发请求,但显式将阻塞工作 offload 到虚拟线程执行器,例如:
CompletableFuture.supplyAsync(() -> {
// 阻塞 I/O、JDBC、RestTemplate 等
}, Executors.newVirtualThreadPerTaskExecutor());
Spring MVC 已经能够处理返回的 CompletableFuture:
- 它会挂起 servlet 处理。
- 注册回调,在 future 完成时恢复执行。
好处
- 细粒度控制——仅将真正需要的部分 offload。
- 依赖
synchronized或ThreadLocal的关键代码仍然在 OS 线程上运行,避免钉住和 ThreadLocal 问题。
缺点
- 需要在阻塞调用的边界处进行代码修改。
- 相比属性式方案更具侵入性;必须审计并包装所有阻塞调用。
选择采用路径
| 标准 | 基于属性(全局) | 手动卸载 |
|---|---|---|
| 所需代码更改 | 无 | 有(包装阻塞调用) |
| 对哪些代码在虚拟线程上运行的控制 | 无(所有请求) | 可选择性 |
对现有 synchronized / ThreadLocal 使用的影响 | 可能有风险(固定、丢失 ThreadLocal 状态) | 可以在 OS 线程上保留关键区段 |
| 操作简易性 | 非常简单(单一属性) | 更复杂(执行器管理) |
| 载体线程耗尽的风险 | 较高(全局固定) | 较低(仅卸载的部分) |
建议
- 如果代码库中的
synchronized块很少且不大量依赖ThreadLocal状态,基于属性的方法提供了最快的可扩展性路径。 - 如果应用程序大量使用
synchronized区段、基于ThreadLocal的安全上下文或其他线程亲和性模式,手动卸载策略更安全,能够将虚拟线程的使用限制在真正的阻塞 I/O 路径上。
快速参考
# Enable virtual threads globally (Spring Boot 3.2+)
server:
servlet:
threads:
virtual-threads-enabled: true
// Manual offload example
Executor vtExecutor = Executors.newVirtualThreadPerTaskExecutor();
CompletableFuture<String> result = CompletableFuture.supplyAsync(() -> {
// Blocking call (e.g., RestTemplate, JDBC)
return restTemplate.getForObject(url, String.class);
}, vtExecutor);
UI‑Res 重写同步和 ThreadLocal‑依赖代码
选项 2:手动卸载到虚拟线程(选择性采用)
描述
- Tomcat 继续使用其默认的 OS‑thread servlet pool。
- 现有代码在 OS 线程上保持不变运行。
- 对 I/O 密集型逻辑显式卸载,使用
CompletableFuture.supplyAsync(task, virtualThreadExecutor);
- Spring MVC 原生支持
CompletableFuture返回类型,能够挂起请求处理、释放 servlet 线程,并在 future 完成时恢复。
ThreadLocal 考虑
卸载会产生一个 硬线程边界。诸如:
SecurityContext- MDC 跟踪数据
这些上下文 不会自动传播,必须手动捕获并恢复。该边界是显式且可控的。
好处
- 保持现有假设(
synchronized、ThreadLocal)。 - 避免在旧代码中出现 carrier‑thread 锁定。
- 只在有益的地方有针对性地使用虚拟线程。
- 支持增量迁移。
- OS‑线程与虚拟线程执行之间的隔离清晰。
权衡
- 需要稍多的样板代码。
- 必须显式进行上下文传播。
- 虚拟线程的使用必须有意识地进行。
后果
| 正面 | 负面 |
|---|---|
| 提升 I/O 密集型端点的可扩展性。 | 为上下文传播增加额外的样板代码。 |
| 无需重构现有的同步代码。 | 需要严格遵守卸载边界的纪律。 |
| 运行时行为可预测。 | |
| 迁移路径清晰。 |
最终结论
基于属性的虚拟线程方案仅适用于已经对虚拟线程友好的代码库。
对于本系统,手动卸载是最安全、最有效的策略,它在保留正确性和运营稳定性的同时,提供了虚拟线程的优势。
04 · 陷阱(生产前阅读)
synchronized 将载体线程固定
问题
- 虚拟线程被粘在载体上。
9个并发请求 +8个载体 → 死锁。
解决方案:使用 ReentrantLock
// Pins carrier
public synchronized Product fetch(String id) {
return restTemplate.getForObject("/p/{id}", Product.class, id);
}
// Safe
private final ReentrantLock lock = new ReentrantLock();
public Product fetch(String id) {
lock.lock();
try {
return restTemplate.getForObject("/p/{id}", Product.class, id);
} finally {
lock.unlock();
}
}
手动卸载期间 ThreadLocal 上下文丢失
会出现的问题
- MDC
SecurityContextRequestAttributes
解决方案:捕获并恢复上下文
Map<String, String> mdc = MDC.getCopyOfContextMap();
SecurityContext sec = SecurityContextHolder.getContext();
return CompletableFuture.supplyAsync(() -> {
if (mdc != null) MDC.setContextMap(mdc);
SecurityContextHolder.setContext(sec);
try {
return service.doWork();
} finally {
MDC.clear();
SecurityContextHolder.clearContext();
}
}, ioExecutor);
💡 当全局使用 virtual-threads-enabled: true 时,此问题 不存在。
使用池化执行器时的 ThreadLocal 泄漏
问题
- 将执行器替换为固定线程池会导致 ThreadLocal 在请求之间泄漏。
解决方案:强制使用正确的执行器
@Bean
public Executor ioExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
本地(JNI)调用静默固定
示例
- 某些 JDBC 驱动
- 加密库
限制影响
private static final ExecutorService NATIVE_POOL =
Executors.newFixedThreadPool(10);
public Future<String> callNative(String input) {
return NATIVE_POOL.submit(() -> nativeLib.process(input));
}
启用固定日志(仅开发环境)
-XX:+PrintVirtualThreadPinning
MVC 与 WebFlux 同时使用
如果两个 starter 都存在,Spring 会选择 MVC,且不会显示警告。
规则
- 虚拟线程 → 保留
spring-boot-starter-web。 - 移除
starter-webflux。
虚拟线程上的 CPU 密集型工作
反模式
- 重度计算
- 图像处理
- 加密循环
正确的划分
// I/O work
CompletableFuture.supplyAsync(
() -> restTemplate.getForObject(...),
Executors.newVirtualThreadPerTaskExecutor());
// CPU work
CompletableFuture.supplyAsync(
() -> heavyComputation(data),
ForkJoinPool.commonPool());
关键要点
Virtual Threads 是 阻塞的、I/O‑密集的、无法重写的 Spring Boot 服务 的最佳选择。
它们为您提供可扩展性、简洁性和生产安全性——无需响应式的复杂性。