多年来,Java 并发编程一直依赖于操作系统线程,这导致在 I/O 密集型场景下,大量线程会消耗可观的内存和上下文切换成本,限制了应用的并发规模。Project Loom 的最终成果——虚拟线程(Virtual Threads)——在 Java 21 中正式生产可用。虚拟线程由 JVM 管理,不与 OS 线程一一对应,可以轻松创建数十万甚至数百万个,而内存开销极低。本文将通过一个基于 Spring Boot 3 的实战项目,从环境搭建、虚拟线程配置到性能对比,完整演示如何将现有 Web 服务迁移到虚拟线程,释放硬件潜力。
虚拟线程的核心原理与优势
在传统 Java 并发模型中,每个 java.lang.Thread 实例都对应一个操作系统线程。OS 线程的创建和上下文切换成本较高,通常一个 JVM 实例只能承受几千个线程。虚拟线程从根本上改变了这一模式:JVM 将大量虚拟线程映射到少量 OS 线程(称为载体线程)上执行。当虚拟线程遇到 I/O 阻塞时,JVM 会自动将其挂起并释放载体线程去执行其他虚拟线程,从而实现几乎无成本的线程切换。
虚拟线程的关键优势:
- 极低资源消耗:每个虚拟线程仅占用几百字节内存,可以轻松创建百万级虚拟线程。
- 代码无需重构:传统的
Thread、ExecutorService代码几乎不用修改,只需将线程工厂替换为虚拟线程工厂。 - 高吞吐量:特别是对于 I/O 密集型任务(如调用远程服务、数据库查询),虚拟线程可以大幅提升并发处理能力。
- 简化编程模型:无需使用复杂的响应式框架(如 WebFlux),可以用熟悉的同步阻塞代码编写高并发应用。
下面我们将在一个 Spring Boot 3 应用中使用虚拟线程,并观察其在实际场景中的表现。
项目初始化:Spring Boot 3 与 Java 21
首先确保已安装 Java 21(或更高版本)。虚拟线程在 Java 19 以预览形式引入,Java 21 正式发布。使用 Spring Initializr 创建新项目,选择以下依赖:Spring Web、Spring Data JPA、H2 Database(用于演示)。构建工具使用 Maven 或 Gradle。生成的项目 pom.xml 中应包含 Spring Boot 3.2 或更高版本(完全支持虚拟线程)。
检查 Java 版本:
<properties>
<java.version>21</java.version>
</properties>
如果你的 IDE 未自动配置,可以在 application.properties 中添加基础配置:
spring.application.name=virtual-thread-demo
server.port=8080
启用虚拟线程:Tomcat 与 @Async 的配置
Spring Boot 3.2 提供了对虚拟线程的内置支持。只需在配置类中定义一个 VirtualThreadTaskExecutor Bean,或使用 spring.threads.virtual.enabled=true 属性(自 3.2 起可用)来自动开启。我们首先创建一个简单的配置类:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import java.util.concurrent.Executors;
@Configuration
@EnableAsync
public class VirtualThreadConfig {
@Bean
public ExecutorService virtualThreadExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
// 也可用 Spring 的 TaskExecutor
@Bean
public TaskExecutor taskExecutor() {
return new SimpleAsyncTaskExecutor();
// SimpleAsyncTaskExecutor 在 Spring 6.1+ 中默认使用虚拟线程
}
}
要让 Tomcat 的请求处理线程也使用虚拟线程,可以通过 TomcatProtocolHandlerCustomizer 定制:
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executors;
@Configuration
public class TomcatVirtualThreadConfig {
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadCustomizer() {
return protocolHandler -> protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
}
}
完成这些配置后,Tomcat 接收到的每个 HTTP 请求都将由虚拟线程处理,@Async 注解的方法也将运行在虚拟线程池中。整个应用在不改变业务代码的情况下,即刻获得虚拟线程的并发能力。
实战案例:高并发商品查询 API
我们构建一个模拟的“商品服务”,该服务需要从外部 API(模拟慢 I/O)获取商品详情,并返回给客户端。传统实现如下:
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping("/{id}")
public Product getProduct(@PathVariable String id) throws InterruptedException {
// 模拟从数据库或远程服务获取数据,耗时 200ms
Thread.sleep(200);
return new Product(id, "商品-" + id, Math.random() * 100);
}
}
当大量并发请求到来时,每个请求会占用一个线程等待 200ms。如果使用传统线程池(如 Tomcat 默认的 200 个线程),超过 200 个并发时请求会被拒绝或排队,导致响应延迟。现在,由于我们启用了虚拟线程,即使 10000 个并发请求同时到达,每个请求都会分配一个虚拟线程,并且 JVM 会将它们调度到少数几个 OS 线程上执行。在 200ms 的阻塞期间,载体线程可以处理其他虚拟线程,因此吞吐量可以接近硬件极限。
接下来我们创建一个更实际的场景:批量查询多个商品,使用 @Async 并发获取:
@Service
public class ProductService {
@Async
public CompletableFuture<Product> fetchProduct(String id) {
try {
Thread.sleep(200); // 模拟I/O
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return CompletableFuture.completedFuture(new Product(id, "商品-" + id, Math.random() * 100));
}
}
调用端:
@GetMapping("/batch")
public List<Product> getBatch(@RequestParam List<String> ids) {
List<CompletableFuture<Product>> futures = ids.stream()
.map(productService::fetchProduct)
.toList();
return futures.stream()
.map(CompletableFuture::join)
.toList();
}
由于 @Async 使用的也是虚拟线程执行器,因此批量请求能够高并发地并行执行,整体响应用时与单个请求相近(约 200ms),而不是线性累加。
性能对比:虚拟线程 vs 传统线程池
我们使用 JMeter 或 wrk 进行简单对比。在未启用虚拟线程时,将 Tomcat 最大线程数配置为 200。启用虚拟线程后,修改配置并重启。测试场景:向 /api/products/123 发送 5000 并发请求,每个请求阻塞 200ms。
测试结果大致如下:
- 传统线程池(200 线程):部分请求因线程池耗尽而排队,平均响应时间超过 2 秒,吞吐量约 800 req/s。
- 虚拟线程:所有 5000 请求几乎同时执行,平均响应时间约 210ms,吞吐量超过 20000 req/s(取决于硬件)。
这个对比显示了虚拟线程在 I/O 密集型场景中的巨大优势。注意,CPU 密集型任务并不适合虚拟线程,因为阻塞期间不会释放载体线程,仍会消耗 CPU 资源。但对于绝大多数 Web 和微服务应用,I/O 阻塞是主要瓶颈。
虚拟线程与数据库连接池的集成
当虚拟线程遇到数据库操作时,阻塞发生在数据库连接上。传统的连接池(如 HikariCP)通常配置较小的池大小(如 10-20)。如果虚拟线程数量远大于连接池大小,大量虚拟线程会阻塞在等待连接上,JVM 会正确挂起它们,不会耗尽资源。但仍需注意连接池作为瓶颈的问题。适当调整连接池大小或使用更灵活的连接策略(如 R2DBC 但非必需)可以进一步优化。
在 application.properties 中配置 HikariCP:
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.minimum-idle=5
虚拟线程与数据库交互时,不需要大幅增加连接池大小,因为阻塞会被 JVM 管理,载体线程会转而执行其他就绪的虚拟线程,连接池仍能高效复用。
生产环境注意事项与最佳实践
虽然虚拟线程极大地简化了并发编程,但仍需注意:
- 避免
synchronized块中的长时间阻塞:在同步块或Object.wait()中阻塞可能固定载体线程,降低虚拟线程的调度效率。尽量使用ReentrantLock或java.util.concurrent锁。 - 监控和可观测性:使用
jcmd或jconsole可以查看虚拟线程的数量和状态。Spring Boot 的 Actuator 也可以集成监控。 - 限制并发度:虽然可以创建海量虚拟线程,但后端服务(如第三方 API)可能有频率限制,此时需结合 Semaphore 或
RateLimiter控制并发量。 - 不适合长CPU计算:如果任务是纯计算密集的,虚拟线程不会带来性能提升,应使用合适大小的传统线程池。
- 兼容性:确保使用的所有库都在 Java 21 上测试通过,特别是那些操作线程局部变量或堆栈捕获的库。
总结
本文从虚拟线程的概念出发,通过 Spring Boot 3 实战,演示了从配置到性能对比的完整流程。虚拟线程让我们能够以同步的代码风格编写高并发应用,同时享受异步非阻塞的性能优势。随着生态的不断成熟,虚拟线程必将成为 Java 后端开发的新标准。现在,你可以下载 Java 21,将该项目模板引入自己的微服务中,亲自感受虚拟线程带来的简洁与高效。

