IWA
2025-11-24
点 赞
0
热 度
5
评 论
0

Java Virtual Threads(虚拟线程)教程:进阶调优与性能压测指南(第 5 篇)

下面是 第 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(非常实用)

下面这一张表,可以直接用于生产环境排查:

检查项

描述

❑ 是否出现大量 platform threads?

若出现 → 你代码中有 pin

❑ 是否使用 synchronized?

替换为 CAS 或并发集合

❑ JDBC 是否虚拟线程友好?

驱动版本必须 ≥ 2023

❑ 连接池是否成为瓶颈?

增加连接池或使用直连

❑ 是否存在 CPU 密集任务?

使用 ForkJoinPool

❑ 是否存在大对象 / 大数组?

降低 memory footprint

❑ I/O 是否大量排队?

分析上下游性能瓶颈

❑ 是否启用了 ZGC?

推荐


八、总结(第 5 篇完成)

本篇内容你已经学会了:

✔ 如何评估虚拟线程的性能瓶颈

✔ 如何诊断 pin 与虚拟线程调度问题

✔ 虚拟线程的主要性能影响因素

✔ 生产环境下的调优方案

✔ 100K~1M 并发压测的通用套路

✔ ZGC、数据库池、HTTP I/O 的性能优化策略

到此,你已经具备:

将虚拟线程用于生产环境的系统级调优能力。


下一篇(第 6 篇)预告

《Java Virtual Threads(虚拟线程)教程:与 Loom 计划的未来演进》

内容包括:

  • 虚拟线程与 Structured Concurrency 的深度结合

  • Loom 未来将引入的 API 与增强

  • Project Loom 的发展路线图

  • 与 Reactor / CompletableFuture 的融合趋势

  • 虚拟线程对云原生、Serverless 的影响



用键盘敲击出的不只是字符,更是一段段生活的剪影、一个个心底的梦想。希望我的文字能像一束光,在您阅读的瞬间,照亮某个角落,带来一丝温暖与共鸣。

IWA

infp 调停者

具有版权性

请您在转载、复制时注明本文 作者、链接及内容来源信息。 若涉及转载第三方内容,还需一同注明。

具有时效性

文章目录

IWA的艺术编程,为您导航全站动态

37 文章数
9 分类数
10 评论数
32标签数
最近评论
IWA

IWA


👍

M丶Rock

M丶Rock


😂

M丶Rock

M丶Rock


感慨了

M丶Rock

M丶Rock


厉害了

M丶Rock

M丶Rock


6666666666666666666