Java 21 虚拟线程:我把线程池换成它之后,服务抗压能力提升了三倍

2026-08-09 0 362

上周我负责的订单服务被运营人员一通吐槽,说大促期间接口经常超时。我看了眼监控,发现线程池被打满了,请求全在排队。翻代码发现项目里用了一个固定大小20的线程池去查外部服务,一个查询要依次调库存、物流、优惠三个接口,每个接口耗时差不多200ms,这样一个任务就得600ms。20个线程一卡,其他请求全堵住。

本来想提高线程数,但数据库连接池有限制,改配置还得重启。后来想起Java 21早就出了虚拟线程,就把那个线程池换成了虚拟线程,代码改动量小到我自己都吃惊。这里把我的实战过程完整记录下来,不是抄官方文档,是真正踩过坑以后的经验。

虚拟线程到底是什么?说白了就是JVM管理的轻量级线程

我们平时用的线程是操作系统线程,创建、切换、阻塞都挺贵。虚拟线程不一样,它由JVM调度,可以在普通线程(载体线程)上挂载和卸载。当虚拟线程遇到IO阻塞时,JVM会自动把它从载体线程上卸下来,载体线程就能去跑别的虚拟线程,等IO结束了再挂回去。

好处就是:你可以创建成千上万个虚拟线程,不用怕把内存搞爆。而且代码写起来特别像普通线程,不需要重启架构。

最基础的用法:Thread.ofVirtual()

直接看代码,创建一个虚拟线程跑一个任务:

Thread thread = Thread.ofVirtual()
        .name("virtual-request-")
        .start(() -> {
            System.out.println("跑在虚拟线程里");
        });
thread.join();

就这么简单。ofVirtual() 返回一个构建器,你可以设置名字和起始任务。如果你更习惯使用线程池,可以用这个:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<String> result = executor.submit(() -> "异步结果");
    System.out.println(result.get());
}

Executors.newVirtualThreadPerTaskExecutor() 会为每次任务创建一个新的虚拟线程,没有池化的概念。很多人会问:没有线程池做复用,性能会不会差?实际上虚拟线程创建成本极低,不值得复用。反而复用虚拟线程会得不偿失。

实战案例:一个订单查询接口的改造

假设我们有一个接口,需要并行查三个外部服务,原来代码用的是固定线程池:

ExecutorService pool = Executors.newFixedThreadPool(20);

public OrderVO getOrderDetail(String orderId) throws Exception {
    Future<InventoryVO> inventoryFuture = pool.submit(() -> inventoryService.query(orderId));
    Future<LogisticsVO> logisticsFuture = pool.submit(() -> logisticsService.query(orderId));
    Future<PromotionVO> promotionFuture = pool.submit(() -> promotionService.query(orderId));

    OrderVO vo = new OrderVO();
    vo.setInventory(inventoryFuture.get());
    vo.setLogistics(logisticsFuture.get());
    vo.setPromotion(promotionFuture.get());
    return vo;
}

线程池大小是20,但是 200个请求同时进来,每个请求占用一个线程去等待三个外部服务,线程马上耗尽。我改成虚拟线程之后,每个请求都自己创建一个虚拟线程,代码变成这样:

public OrderVO getOrderDetail(String orderId) throws Exception {
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        Future<InventoryVO> inventoryFuture = executor.submit(() -> inventoryService.query(orderId));
        Future<LogisticsVO> logisticsFuture = executor.submit(() -> logisticsService.query(orderId));
        Future<PromotionVO> promotionFuture = executor.submit(() -> promotionService.query(orderId));

        OrderVO vo = new OrderVO();
        vo.setInventory(inventoryFuture.get());
        vo.setLogistics(logisticsFuture.get());
        vo.setPromotion(promotionFuture.get());
        return vo;
    }
}

try-with-resources 确保所有虚拟线程执行完毕,才关闭executor。注意这里 close() 方法会等待所有任务完成,符合我们的语义。

实际压测效果很明显。原来20个线程,每秒只能通过大约30个请求(因为每个请求要占三个线程等600ms)。换用虚拟线程后,同时可以处理的虚拟线程数几乎不受限制,每秒能通过的请求直接涨到90左右。而且我没有改任何数据库连接池,因为真正占用的还是只有三个外部服务连接。

坑一:千万别用虚拟线程池化

我看到有人还在用 Executors.newFixedThreadPool(1000) 那种方式去包虚拟线程,这完全没必要。你可以把虚拟线程理解为“用完即丢”,创建成本几乎是零。如果你把它们池化,反而限制了并发能力,而且代码看起来不伦不类。正确用法就是每次任务创建一个虚拟线程。

坑二:synchronized会让你踩坑

如果你在虚拟线程里用了 synchronized 代码块,并且这个同步块里有IO阻塞操作,那么虚拟线程不会自动卸载,它会钉住载体线程。这可能会导致载体线程被占满。解决办法很简单:尽量用 ReentrantLock 代替 synchronized,或者让 synchronized 块保持很短,里面别做IO。

举一个我碰到的例子:一个旧代码里方法加了 synchronized,里面调了一个远程服务。换成虚拟线程后,并发一高反而出现了一些线程阻塞。排查半天,最后发现在 synchronized 块内调用了外部接口。改成 ReentrantLock 后问题就消失了。

坑三:ThreadLocal要小心

在普通池化线程里,ThreadLocal通常用于传递上下文,比如用户信息、traceId。但虚拟线程数量巨大,而且很多是短生命周期,如果你用了ThreadLocal忘记清理,会占用大量内存。尤其是容器里常用的 ThreadLocal<Map> 这种,每个虚拟线程都会存一份,最后内存就爆了。

替代方案是使用 ScopedValue,这是Java 24里面的新玩意,Java 21用户暂时还是少用ThreadLocal为妙。如果实在要用,记得在 finally 里 remove。

坑四:不要依赖虚拟线程的执行顺序

虚拟线程虽然是顺序创建的,但调度顺序不受保证。代码里如果隐含有先后依赖,记得用 Future.get() 或者手动等待。不要用 Thread.sleep() 去强行控制,这不靠谱。

如何把现有项目平滑迁移到虚拟线程

我第一次迁移没有直接把所有线程池都推翻,而是从几个关键业务开始试点。步骤大概是:

先找代码里的 ExecutorService 声明,看它们是用来做并行调用还是做异步任务。对于那种“一个任务拆成多个子任务,最后合并结果”的场景,最适合改虚拟线程。对于真正的后台定时任务,比如每5分钟跑一次的批处理,没必要改。

把普通的 newFixedThreadPool(20) 直接替换成 Executors.newVirtualThreadPerTaskExecutor(),然后删除原来的线程池变量。如果你原来用 ThreadPoolExecutor 自定义了队里和拒绝策略,改成虚拟线程后这些统统不需要了,代码还更简单。

改完以后把依赖 synchronized 的代码顺手检查一遍,尤其是那些在同步块里调RPC、HTTP的地方。这种地方是最容易埋雷的。

然后测试环境压一波接口,看看RT和吞吐量的变化。我自己的项目改造完以后,吞吐量确实上去了,但内存占用也稍微高了一点点,其实主要是虚拟线程的栈区,不过比起它带来的并发能力提升,这点内存完全值。

总结一下我的真实感受

虚拟线程并没有改变Java的并发模型,它只是让“每个任务一个线程”变成了一种可落地的选项。以前我们要不断调线程池大小、调队列长度、处理拒绝策略,现在这些通通不用管。你把“一个任务对应一个线程”这套写出来,剩下的交给JVM。

现在我的订单服务已经全面切到虚拟线程了,代码看着清爽,线上也稳定。如果你还在用Java 8或11,也许会觉得这些和自己没关系。但升级到Java 21其实没那么困难,项目里大多数代码都能无缝跑通。我觉得这波升级,值。

Java 21 虚拟线程:我把线程池换成它之后,服务抗压能力提升了三倍
收藏 (0) 打赏

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

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

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

淘吗网 java Java 21 虚拟线程:我把线程池换成它之后,服务抗压能力提升了三倍 https://www.taomawang.com/server/java/2512.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

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

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