下面是 第 5 篇:《Java Virtual Threads(虚拟线程)教程:进阶调优与性能压测指南》
这是本系列中最偏向实战性能优化的内容,适用于对虚拟线程已经掌握并希望进一步优化生产环境性能的工程师。
系列主题:Java Virtual Threads(虚拟线程)教程:从零上手到实战优化
一、虚拟线程性能优化的核心理念(一定要理解)
虚拟线程与传统线程最大的区别之一是:
虚拟线程本身几乎不需要调优,调优的是你代码的阻塞点与运行环境。
换句话说:
虚拟线程性能好不好,不取决于你创建了多少虚拟线程,而取决于:
CPU 核心数
I/O 类型(DB、HTTP、RPC)
阻塞行为是否可挂起(还涉及 JDK 内部是否自动插入“safepoint”)
是否使用锁
驱动/数据库池是否成为新瓶颈
从宏观来看:
传统调优(ThreadPool)
→ 调线程池大小、队列、拒绝策略
虚拟线程调优
→ 优化锁、减少竞争、优化同步结构、优化 I/O 阻塞点、减少上下文切换
二、虚拟线程的性能影响因素与调优建议
1. 避免在虚拟线程中使用重量级锁(非常重要)
Bad:
synchronized (lock) {
// do work
}
更坏:
ReentrantLock lock = new ReentrantLock();
lock.lock();
// do work
lock.unlock();
原因:
重量级锁会“pin”住虚拟线程,使其不能挂起(会强制绑定平台线程)
会极大降低并发能力(虚拟线程失去意义)
✔ 推荐方案:使用无锁结构或并发集合
LongAdder
ConcurrentHashMap
AtomicInteger / AtomicLong
StampedLock(读多写少有优势)
CAS + Retry
2. 不要在虚拟线程里做 CPU 密集型任务
虚拟线程不会提升 CPU 性能。
例如:
executor.submit(() -> {
for(int i=0; i<10_000_000; i++){
// heavy calc
}
});
CPU 密集型任务应该:
使用 ForkJoinPool
或使用 platform threads(
Thread.ofPlatform())或拆分任务使用 parallel streams
3. 避免在虚拟线程中频繁创建大对象或分配大内存
虚拟线程数量本身可以很大,但 JVM 堆大小有限。
坏例子:
executor.submit(() -> {
byte[] big = new byte[50 * 1024 * 1024];
});
建议:
对每个任务的数据保持小内存 footprint
尽量不要让虚拟线程负担大量堆内存
使用 G1 或 ZGC(虚拟线程更适配 ZGC)
4. 避免未被“虚拟线程化”的底层调用
一些底层 Java/C++ 方法不会自动生成可挂起点(pin)。例如:
使用旧版本 JDBC 驱动
使用 old I/O(非 NIO)
使用过多 synchronized
使用本地方法(JNI)
例如:
socket.getInputStream().read() // 旧 I/O,会阻塞平台线程
✔ 建议:
JDK21 及后续版本尽量使用经过优化的 I/O
使用 HTTP Client(java.net.http)
关注你的数据库驱动版本是否支持虚拟线程友好模式
5. 避免把虚拟线程卡在外部资源池
例如 JDBC 连接池:
当你有 100 万个虚拟线程,但连接池只有 200 个连接:
1000000 个虚拟线程
↓
200 个数据库连接限制
会产生排队:
虚拟线程在排队时是挂起,不消耗平台线程
但会导致吞吐量不升反降
✔ 建议调优策略:
减少每个虚拟线程的 DB 请求次数
增加连接池大小(但不要超过数据库能支撑的并发连接)
某些时候可以采用 无连接池方案(特别是 PostgreSQL/pgBouncer 场景)
三、如何判断虚拟线程是否被 pin?(非常关键)
“pin” 意味着:
虚拟线程绑定到平台线程,不能挂起,失去轻量化优势。
检测方法 1:JDK Flight Recorder(官方推荐)
运行应用时加上:
-XX:StartFlightRecording=filename=record.jfr
在 JFR 中搜索:
jdk.VirtualThreadPinned
出现大量此事件 → 表示你的代码使用了 不可挂起点。
检测方法 2:使用 JDK 命令(JDK 21+)
jcmd <pid> VM.native_memory summary
如果 platform thread 数异常升高(几十、上百),说明虚拟线程正在转换为平台线程。
四、虚拟线程性能压测:如何正确测?(全程实战)
虚拟线程压测需要遵循三个原则:
原则 1:使用大量 I/O 模拟真实场景
例如 HTTP 请求或 DB 查询:
executor.submit(() -> {
HttpClient client = HttpClient.newHttpClient();
client.send(
HttpRequest.newBuilder(URI.create("http://localhost/test"))
.GET().build(),
HttpResponse.BodyHandlers.ofString()
);
});
原则 2:压测时压力要足够大
例如:
10K 线程(传统线程几乎不可能)
100K 线程
500K 线程
100 万线程(虚拟线程的真正优势)
测试代码如下:
public class VTLoadTest {
public static void main(String[] args) throws Exception {
int count = 100_000;
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = new ArrayList<Future<?>>();
for (int i = 0; i < count; i++) {
futures.add(executor.submit(() -> {
Thread.sleep(1000); // 模拟 I/O
return null;
}));
}
for (var f : futures) {
f.get();
}
}
}
}
测试结果通常是:
100k 虚拟线程:几十 MB 内存
100k 平台线程:直接 OOM 或启动缓慢
原则 3:观察三个关键指标
指标 1:每秒请求数(TPS)
越高越好。
指标 2:虚拟线程数量
通过:
jcmd <pid> Thread.print
指标 3:平台线程数量
虚拟线程调度通常只需要:
CPU 核心数量 × 1~2 个平台线程
如果 platform threads 持续增高 → 出现 pin。
五、生产环境中的虚拟线程调优策略
你可以根据场景直接套用(非常实用)。
1. Web 服务(Spring Boot、Vert.x)
✔ 建议使用虚拟线程处理 HTTP 请求
✔ 避免阻塞式同步锁
✔ 避免一次请求触发过多 DB 查询
✔ 使用合适连接池
Spring Boot 3 配置虚拟线程:
@Bean
public ThreadFactory virtualThreadFactory() {
return Thread.ofVirtual().factory();
}
@Bean
public Executor taskExecutor(ThreadFactory factory) {
return Executors.newThreadPerTaskExecutor(factory);
}
2. 微服务 RPC(OpenFeign、Dubbo)
RPC 本质是 I/O → 完美适配虚拟线程。
✔ 增加客户端超时
✔ 避免同步锁
✔ 限制链路中 DB 调用次数
3. 高并发批处理系统(ETL、爬虫)
✔ 使用虚拟线程处理海量任务
✔ 避免将大对象丢给虚拟线程
✔ 限制内部阻塞点(例如 Redis、DB)
✔ 避免过多 CPU 密集代码
六、配置 JVM:虚拟线程推荐参数(官方最佳实践)
通常虚拟线程不需要调线程池,因此 JVM 参数相对简单。
推荐:
-XX:+UseZGC
-XX:+UnlockExperimentalVMOptions
-XX:+UseDynamicNumberOfGCThreads
-XX:+EnableJVMCI
理由:
ZGC 对虚拟线程 stack 处理更高效
可自动调整 GC 线程
更适配高并发场景
七、完整调优 checklist(非常实用)
下面这一张表,可以直接用于生产环境排查:
八、总结(第 5 篇完成)
本篇内容你已经学会了:
✔ 如何评估虚拟线程的性能瓶颈
✔ 如何诊断 pin 与虚拟线程调度问题
✔ 虚拟线程的主要性能影响因素
✔ 生产环境下的调优方案
✔ 100K~1M 并发压测的通用套路
✔ ZGC、数据库池、HTTP I/O 的性能优化策略
到此,你已经具备:
将虚拟线程用于生产环境的系统级调优能力。
下一篇(第 6 篇)预告
《Java Virtual Threads(虚拟线程)教程:与 Loom 计划的未来演进》
内容包括:
虚拟线程与 Structured Concurrency 的深度结合
Loom 未来将引入的 API 与增强
Project Loom 的发展路线图
与 Reactor / CompletableFuture 的融合趋势
虚拟线程对云原生、Serverless 的影响
默认评论
Halo系统提供的评论