Java 21虚拟线程实战:把I/O密集吞吐量提升3倍,我踩了哪三个坑

2026-08-16 0 214

今年我对公司一个老项目做性能调优。接口就是查数据库,然后调一次外部RPC,平均耗时要 120ms,基本全是 I/O 等待。老代码用 Tomcat 的 200 线程池处理,压测时一旦并发超过 500,吞吐量就疯狂抖动,RT 也飙到好几秒。我试着把线程池调到 1000,内存快扛不住了,上下文切换也把 CPU 打满。

后来换成 Java 21 的虚拟线程,改了不到 20 行配置,压测显示吞吐量提升了接近 3 倍。不过中间也不是一帆风顺,今天把实际过程、代码和踩坑记录一下,给想动迁的朋友一个参考。

简单理解虚拟线程,不是魔法而是“按需调度”

以前一个 Java 线程就对应一个操作系统线程,创建和切换成本都不小。虚拟线程是 JVM 自己管理的轻量级线程,它的成本比平台线程低得多,你可以创建几十万个而不用太担心内存。阻塞 I/O 发生时,虚拟线程会自动让出底层载体线程,让别的虚拟线程跑,从而大幅提高载体线程的利用率。也就是说,你不用再为了“防阻塞”而强行上 NIO 或者改响应式,普通同步代码也能享受高并发。

有个前提:虚拟线程适合那种大量等待且 I/O 密集的应用,如果你的是 CPU 密集计算,那虚拟线程帮不上忙,反而可能因为多一层调度降低性能。

改造第一步:让 Spring Boot 用上虚拟线程

项目是 Spring Boot 3.2,正好支持虚拟线程。首先在配置里开启:

spring.threads.virtual.enabled=true

这会自动让 Tomcat 使用虚拟线程来处理请求,同时 Spring 的 @Async 也会默认用虚拟线程执行。就这么一句,很多以阻塞 I/O 为主的接口立刻改善。但只有这一句还不够,我希望能跑的更极致,于是自己定义了一个虚拟线程的 TaskExecutor,用来跑一些定时任务和异步任务。

@Configuration
public class VirtualThreadConfig {

    @Bean
    public AsyncTaskExecutor applicationTaskExecutor() {
        return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
    }
}

注意不要在每个地方都 new 一个虚拟线程池,最好全局复用。官方建议用 Executors.newVirtualThreadPerTaskExecutor(),它会按需创建虚拟线程,并且复用载体线程。

我还写了一个简单的模拟接口,用来压测:

@RestController
public class DemoController {

    @GetMapping("/demo")
    public String demo() throws InterruptedException {
        // 模拟 DB 查询 50ms
        Thread.sleep(50);
        // 模拟外部 RPC 70ms
        Thread.sleep(70);
        return "ok";
    }
}

这个接口没有任何优化,纯阻塞等待,但正是虚拟线程的用武之地。

压测结果:吞吐量确实上去了

为了公平,我在同一台机器上,用相同压测并发(1000并发,300秒),只改了启动参数和配置。老线程池 200 时,吞吐量大概 1600 QPS,RT p99 为 1.2 秒。换成虚拟线程后,吞吐量达到 4700 QPS,RT p99 降到 180 毫秒。这个提升在预期内,因为每个请求大部分时间都是 sleep 等待,虚拟线程把载体线程利用率翻了好几倍。

更直观的感受是,线程数从原来 200 个平台线程变成几万个虚拟线程,但内存占用几乎没有增加。JVM 里平台线程数量还是几十个,虚拟线程的栈是在堆里分配的,小很多。

坑一:ThreadLocal 在虚拟线程里是大坑

虚拟线程数量多,如果每个虚拟线程都用了 ThreadLocal 存放了比较大的对象,内存可能会爆炸。项目里有个老代码用 ThreadLocal 缓存用户权限信息,原来 200 线程最多 200 份,现在瞬间涌进一万个虚拟线程,可能就有上万份大对象,GC 直接被压垮。

我的解决方法很简单:先从 ThreadLocal 里取,如果取不到,就去缓存里查一次,然后写回 ThreadLocal。但重点是,要在 finally 里 remove(),防止虚拟线程被重复使用时读到脏数据。写个过滤器统一处理,别偷懒。

try {
    // 业务逻辑
} finally {
    userContext.remove();
}

坑二:synchronized 可能会 pin 住载体线程

在虚拟线程里使用 synchronized 关键字,如果锁竞争严重,JVM 可能会把载体线程给“钉住”(pin),也就是阻塞住了底层平台线程。老代码里很多同步块用的是 synchronized,我一开始没改,压测时发现吞吐量只是微升。后来用诊断工具看到载体线程被 pin 得厉害。

解决办法是把锁换成 ReentrantLock。ReentrantLock 在等待时不会 pin 住虚拟线程,能够主动让出载体线程。

private final ReentrantLock lock = new ReentrantLock();

public void doSomething() {
    lock.lock();
    try {
        // 临界区
    } finally {
        lock.unlock();
    }
}

尤其注意对第三方库内嵌的 synchronized,无法修改代码时就只能绕开或者升级版本。比如某些旧版数据库连接池内部用 synchronized,也容易 pin。我后来换用 HikariCP 的最新版,它内部改用了 StampedLock 等,兼容性更好。

坑三:不要在虚拟线程里做 Thread.yield() 之类的老把戏

以前为了提高线程池利用率,有人会在阻塞代码前加 Thread.yield() 或者临时把优先级调低。这些在虚拟线程里不但没用,还会干扰调度器。虚拟线程的调度是 JVM 控制的,不要想着手动干预。我一开始保留了几个 Semaphore 信号量来控制并发数,结果发现虚拟线程在等待信号量时也会自动释放载体线程,根本不需要自己写限流配置。后面我把手动限流全删了,直接用 Semaphore 配合虚拟线程,既简单又高效。

更激进一点:不用 Spring,直接用虚拟线程写个并发任务器

为了验证虚拟线程在数据抓取场景的效果,我用纯 Java 写了个小工具:从 1000 个 URL 抓取内容,以前用线程池,现在用虚拟线程:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<String>> futures = urls.stream()
            .map(url -> executor.submit(() -> fetchUrl(url)))
            .toList();
    for (Future<String> f : futures) {
        System.out.println(f.get().length());
    }
}

我这里使用了 try-with-resources,它会等待所有提交的任务执行完,再关闭 executor。这代码简洁到有点不真实,但实际跑起来抓取和解析确实很快。虚拟线程甚至省掉了线程池大小的纠结,来多少任务就起多少虚拟线程,底层载体线程自动复用。

总结:虚拟线程不是银弹,但确实适合大多数业务后端

这次迁移让我最大的感触是:绝大多数 Web 应用都属于 I/O 密集型,而虚拟线程能让同步编程模型获得接近异步的性能,同时还不用改业务代码。代价就是要重新审视 ThreadLocal、synchronized 和旧库的兼容性。

如果让我给建议,我会说:先去试试,遇到问题用 JFR 或者 async-profiler 看看有没有 pin 的线程,把 synchronized 和 ThreadLocal 换掉,虚拟线程基本就能跑得很稳。今天的项目还没上生产?没关系,Java 21 已经发布很久了,Spring Boot 3.2 也支持得不错,是时候考虑把它提上日程了。

Java 21虚拟线程实战:把I/O密集吞吐量提升3倍,我踩了哪三个坑
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 java Java 21虚拟线程实战:把I/O密集吞吐量提升3倍,我踩了哪三个坑 https://www.taomawang.com/server/java/2555.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务