回顾 Java 传统线程模型的瓶颈及 Project Loom 的设计动机,接着深入剖析虚拟线程的原理、使用方式及调度机制,并结合多种高并发场景给出实战示例。
目录
背景与动机
在传统 Java 并发模型中,每个 java.lang.Thread
都映射到一个操作系统线程(Platform Thread),线程数受限于 OS 资源,且单个线程阻塞时会占用整个 OS 线程,导致大并发场景下资源浪费和性能瓶颈。为了解决这一问题,OpenJDK 社区发起了 Project Loom,引入“虚拟线程”(Virtual Threads),以 JVM 层面的轻量级线程来实现大规模并发。
虚拟线程简介
什么是虚拟线程
虚拟线程是一种由 JVM 调度的轻量级线程类型,不再与 OS 线程一一对应,而是通过“载体线程”(Carrier Threads)复用底层操作系统线程资源。当虚拟线程被挂起(如遇到阻塞 I/O),它会释放载体线程,使该载体线程可以继续执行其他虚拟线程,从而极大地提升并发度和资源利用率。
JEP 425 与 JEP 444 概览
- JEP 425: Virtual Threads (Preview) — 于 Java 19 引入预览,初步实现虚拟线程的 API 和运行时支持。
- JEP 444: Virtual Threads — 于 Java 21 正式稳定,虚拟线程成为标准特性并优化了调度器与阻塞处理机制。
虚拟线程工作原理
Carrier Threads 与挂载机制
JVM 内部维护一组载体线程(Carrier Threads),用于实际执行虚拟线程的任务。虚拟线程在准备执行时“挂载”到某个载体线程,执行完毕或遇阻塞时“卸载”挂回队列,载体线程可立即切换到其他挂起的虚拟线程。
阻塞调用的处理
虚拟线程遇到阻塞调用(如网络 I/O、文件读写)时,JVM 会自动将该虚拟线程解除挂载,释放载体线程资源;待 I/O 就绪后,再次挂载并继续执行,用户无需显式使用异步回调或框架,从而简化编程模型。
在项目中使用虚拟线程
启用预览特性
在 pom.xml
或编译命令中加入 --enable-preview
并指定模块:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>21</release>
<compilerArgs>
<arg>--enable-preview</arg>
<arg>--add-modules</arg>
<arg>jdk.incubator.concurrent</arg>
</compilerArgs>
</configuration>
</plugin>
同时在运行时添加 --enable-preview
参数。
创建与管理虚拟线程
方法一:Thread API
Thread vThread = Thread.ofVirtual().start(() -> {
// 并发任务逻辑
});
vThread.join();
方法二:Executor API
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = IntStream.range(0, 1000)
.mapToObj(i -> executor.submit(() -> fetchData(i)))
.collect(Collectors.toList());
for (Future<String> f : futures) {
System.out.println(f.get());
}
}
以上两种方式均可在短代码路径内创建成千上万的虚拟线程。
高并发实战案例
HTTP 服务器模拟
使用 Java 内置的 HttpServer
,采用虚拟线程处理每个连接:
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/", exchange -> {
Thread.sleep(10); // 模拟阻塞
byte[] resp = "Hello, Loom!".getBytes();
exchange.sendResponseHeaders(200, resp.length);
exchange.getResponseBody().write(resp);
exchange.close();
});
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.start();
在同一台 8 核机器上,虚拟线程模式下可轻松处理百万级并发连接,而平台线程模式往往在几万并发后 OOM。
数据库连接池优化
对于每个请求新建虚拟线程,自由开闭 JDBC 连接:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
try (Connection conn = ds.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
while (rs.next()) {
// 处理数据
}
}
});
}
虚拟线程释放载体线程时,底层 JDBC 驱动的阻塞调用不再阻塞主线程池,显著提升吞吐。
性能与对比
与平台线程的内存对比
- 平台线程:每个线程默认保留 1 MB 堆栈;大并发下内存消耗高。
- 虚拟线程:默认仅保留 ~1 KB 的轻量栈,实际按需分配,百万级线程内存消耗可控制在 几十 MB 以内。
与 Reactor/Netty 的编程复杂度对比
Reactor/Netty 等异步框架需使用回调或响应式流,链式编程模型较难调试;虚拟线程可保留同步编程风格,且无显式回调,易读易维护。
调度与限流策略
在高并发场景中,可结合 Semaphore
、限流框架(如 Resilience4j)对虚拟线程并发量进行控制,防止依赖系统资源(数据库、API)过载。例如:
Semaphore semaphore = new Semaphore(100);
Executors.newVirtualThreadPerTaskExecutor().submit(() -> {
if (semaphore.tryAcquire()) {
try {
// 业务处理
} finally {
semaphore.release();
}
} else {
// 拒绝或降级逻辑
}
});
与常见框架的集成
Spring Boot 中的虚拟线程支持
Spring Boot 3.2+ 可通过配置 TaskExecutor
使用虚拟线程:
@Bean
public TaskExecutor taskExecutor() {
return new ConcurrentTaskExecutor(Executors.newVirtualThreadPerTaskExecutor());
}
对 @Async
方法、WebFlux 等均可无缝支持虚拟线程。
Quarkus 与虚拟线程
Quarkus 通过 @Blocking
注解结合虚拟线程执行阻塞操作,并可在 application.properties
中开启:
quarkus.virtual-threads.enabled=true
无需额外代码,即可获得虚拟线程优势。
最佳实践与注意事项
- 适用场景:I/O 密集型、高并发请求场景;CPU 绑定任务建议使用并行流或线程池。
- 线程本地存储:避免过度使用
ThreadLocal
,推荐使用 Scoped Values(JEP 464)管理上下文。 - 调试与监控:使用 JFR(Java Flight Recorder)和
jstack
可查看虚拟线程状态;注意开启-Djfr
支持。 - 逐步迁移:可先在次要模块开启虚拟线程,验证性能与稳定性后再全量推广。