Java虚拟线程实战:没有线程池,一台机器怎么扛住十万并发

2026-09-02 0 550

做个实验。你拿一台4核8G的机器,开200个平台线程,每个线程去睡一觉(模拟IO阻塞),看它同时能扛住多少并发。说实话,200个平台线程处理200个并发请求,已经有点捉襟见肘了。但如果把平台线程换成虚拟线程,你会发现——十万个虚拟线程同时阻塞也没有压力。这不是玄学,是JDK 21正式发布的Virtual Threads。

今天这篇文章不聊那种PPT级别的概念,直接实战。内容包括虚拟线程的核心API、在Spring Boot 3里的集成方式、一个真实场景下的性能对比,以及我踩过的坑。我会把代码和配置原原本本贴出来,你照着做一遍就明白了。

虚拟线程到底解决了什么问题

聊虚拟线程之前,得先说说Java并发编程的老大难问题。

从Java 1.0开始,Thread就相当于一个操作系统线程的封装。每个平台线程(Platform Thread)都要占用独立的栈空间,默认1MB左右。这意味着你创建1万个线程,光栈内存就得10GB,这谁顶得住?所以传统做法就是用线程池,把线程数量控制在200~500个,然后用异步编程来做高并发。

异步编程的代价是可读性和可维护性。你一个简单的同步逻辑,为了异步,得拆成CompletableFuture链式调用,或者用响应式编程,写出来的代码自己看着都头疼。调试的时候,更是一不小心就陷入回调地狱的泥潭。

虚拟线程策略不同:它一个进程可以有几十万个虚拟线程,每个占用内存只有几百字节。你写普通同步代码就行,JVM在遇到阻塞的瞬间自动把虚拟线程挂起,然后把载体线程(Carrier Thread)让给别的虚拟线程执行。

说白了,虚拟线程让你用同步的写法,拿到异步的性能。这个价值在IO密集型项目中是巨大的。

核心API:就这几个,别怕

JDK 21是LTS版本,虚拟线程API已经稳定。创建虚拟线程有三种方式,我逐个说。

1. Thread.ofVirtual()

Thread vt = Thread.ofVirtual()
        .name("order-vt")
        .start(() -> {
            System.out.println("当前执行线程: " + Thread.currentThread());
        });

2. Thread.startVirtualThread()

Thread.startVirtualThread(() -> System.out.println("快速启动一个虚拟线程"));

3. Executors.newVirtualThreadPerTaskExecutor()

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> System.out.println("虚拟线程执行任务"));

关键点:Executors.newVirtualThreadPerTaskExecutor()它不干线程复用的事。每个任务提交进去,都新创建一个虚拟线程,任务跑完就销毁。这个执行器只是个任务提交的入口而已。

所以,别想着用这个执行器来做线程池限流。虚拟线程天然就该是”一个任务一个线程”的模型。

在Spring Boot 3中启用虚拟线程

Spring Boot 3.2以上版本,已经内置了虚拟线程的自动配置。你要做的,就是改一行配置文件。

spring:
  threads:
    virtual:
      enabled: true

这一行配置加进去,Spring Boot底层的Tomcat容器就会使用虚拟线程来处理HTTP请求。对业务代码而言,你是无感知的,原来怎么写还是怎么写。

我随便写一个Controller做演示:

@RestController
@RequestMapping("/order")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/detail")
    public Result getOrderDetail(Long orderId) throws Exception {
        // 下游调了用户服务、优惠券服务、物流服务
        // 没启用虚拟线程之前,这个请求会占用Tomcat线程200ms
        // 启用虚拟线程之后,阻塞期间线程被释放出去处理别的请求
        OrderDetail detail = orderService.getFullDetail(orderId);
        return Result.ok(detail);
    }
}

有一个细节需要注意。如果你用的是Spring Boot 3.1或更早版本,加上这个配置是不生效的。还有,如果你在代码里用了自定义的Servlet Filter,并且依赖了Tomcat线程的数量(比如限流),那么启用虚拟线程后,HttpServletRequest的处理逻辑已经跑在虚拟线程上了,这个变化要心里有数。

性能测试:传统线程池 vs 虚拟线程

空谈没有意义。我写了一个非常直观的对比测试,模拟一个处理了200ms IO阻塞的任务,分别用传统固定线程池和虚拟线程执行800个任务,看总耗时。

先看模拟任务代码:

static void handleTask(int id) throws Exception {
    // 模拟调用下游HTTP接口
    Thread.sleep(200);
}

传统线程池:200个平台线程

public static void fixedThreadPoolTest() throws Exception {
    ExecutorService pool = Executors.newFixedThreadPool(200);
    for (int i = 0; i < 800; i++) {
        int id = i;
        pool.submit(() -> {
            try {
                handleTask(id);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
    pool.shutdown();
    pool.awaitTermination(1, TimeUnit.MINUTES);
}

200个线程并发执行,800个任务分4批执行,每批200ms,总耗时要800ms左右。

虚拟线程:800个虚拟线程全部并发

public static void virtualThreadTest() throws Exception {
    try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
        for (int i = 0; i < 800; i++) {
            int id = i;
            executor.submit(() -> {
                try {
                    handleTask(id);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

800个任务,每个任务一个虚拟线程,全部同时执行,总耗时200ms左右,比传统线程池快了整整4倍。

这个性能优势是怎么来的?因为传统线程池里,线程在Thread.sleep(也就是IO阻塞)的时候,是真正被操作系统挂起的。而虚拟线程在阻塞时,虚拟线程会被JVM从载体线程上卸下来,载体线程被释放去执行其他虚拟线程。所以虚拟线程的阻塞成本接近于零。

实战:一个聚合接口的并发改造

光造轮子没意思。来看一个实际场景。

假设我有一个报表接口,需要同时从三个下游服务拉数据:用户基本信息、订单列表、库存快照。串行调用的话,总耗时是三个接口耗时的总和。并发调用的话,总耗时接近最慢的一个接口。

传统写法用CompletableFuture配合线程池:

@Service
public class DashboardService {

    private final UserServiceClient userClient;
    private final OrderServiceClient orderClient;
    private final InventoryServiceClient inventoryClient;

    private final Executor executor = Executors.newFixedThreadPool(10);

    public DashboardData getDashboard(Long userId) {
        CompletableFuture<UserInfo> userFuture =
                CompletableFuture.supplyAsync(() -> userClient.getUser(userId), executor);
        CompletableFuture<List<Order>> orderFuture =
                CompletableFuture.supplyAsync(() -> orderClient.listOrders(userId), executor);
        CompletableFuture<Inventory> inventoryFuture =
                CompletableFuture.supplyAsync(() -> inventoryClient.getInventory(userId), executor);

        DashboardData data = new DashboardData();
        data.setUser(userFuture.join());
        data.setOrderList(orderFuture.join());
        data.setInventory(inventoryFuture.join());
        return data;
    }
}

换成虚拟线程之后,改一行Executor声明就行:

private final Executor executor = Executors.newVirtualThreadPerTaskExecutor();

这段代码的好处是:配合Spring Boot 3.2的虚拟线程自动配置,你的Tomcat线程池本身就是虚拟线程,而这段业务代码里的CompletableFuture里也用了虚拟线程,整个请求链路没有任何阻塞耗费在线程复用上的资源。很舒服。

当然你可能会问,既然Spring框架里已经是虚拟线程了,为什么CompletableFuture还要单独配?因为在这段代码里,CompletableFuture默认使用ForkJoinPool.commonPool(),如果不指定Executor,它不会自动使用虚拟线程。所以需要显式传入一个虚拟线程执行器。

踩坑记录:虚拟线程不是万能的

我在实际项目中用虚拟线程也踩了不少坑。这里把最值得说的4个坑列出来。

坑1:CPU密集型任务别凑热闹

虚拟线程強在前,在IO阻塞的时候自动挂起,来提升并发度。但如果你的任务是纯CPU计算,比如图像处理、复杂加密、大列表排序,那就算了。因为虚拟线程调度本身也有成本,CPU核数就那么多,虚拟线程也得排队执行,而且频繁切换没有意义。

这类任务老老实实用一个固定大小等于CPU核数的线程池就完了。

坑2:synchronized在JDK 21里会固定载体线程

这是我最无语的一个点。在JDK 21的虚拟线程中,如果遇到synchronized块竞争锁,虚拟线程不会正常卸载,而是会把载体线程整个阻塞住。这意味着其他虚拟线程也跑不了。

解决方式是改用ReentrantLock,或者在JDK 22及以上版本中运行。JDK 22对synchronized和虚拟线程的适配已经做了优化。所以生产环境如果必须用虚拟线程,我建议直接用JDK 21还是JDK 22,或者升级到JDK 23。

坑3:ThreadLocal别乱用

虚拟线程支持ThreadLocal,但虚拟线程创建成本极低,意味着可以创建海量虚拟线程。如果每个虚拟线程都往ThreadLocal里塞大对象(比如用户会话、数据库连接),那内存压力会非常大。

建议用ScopedValue。当然ScopedValue还是Preview Preview,用在生产环境要谨慎。

坑4:Spring Boot版本必须对齐

Spring Boot 3.0、3.1根本不支持spring.threads.virtual.enabled这个配置。只有3.2及以上版本才支持。而且你最好用的是Spring Boot 3.2.5以上版本,早期3.2版本中虚拟线程和Tomcat的配合有Bug,比如连接超时失效的问题。

如果你还在用Spring Boot 2.x,那基本上不用考虑虚拟线程了。先升级到Spring Boot 3.x再说。

总结

虚拟线程是Java并发编程的一次关键升级。它最大的价值是,让普通Java开发者用同步代码,就能写出现在异步编程才能达到的并发性能。说白了就是,把并发编程的门槛降下来了。

我非常推荐你在新的后端项目里尝试一下虚拟线程。如果项目已经用了Spring Boot 3.2以上版本,那么加一行配置就能启用。先跑一个不那么重要的业务接口做下压测,感受一下效果,再慢慢推广。

反正自从我把手头项目的核心服务切到虚拟线程之后,我就再也不想回去写CompletableFuture链式调用了。

这篇文章是我实际使用虚拟线程的一个汇总。哪里写得不对,或者你有踩到别的坑,欢迎留言一起讨论。

Java虚拟线程实战:没有线程池,一台机器怎么扛住十万并发
收藏 (0) 打赏

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

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

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

淘吗网 java Java虚拟线程实战:没有线程池,一台机器怎么扛住十万并发 https://www.taomawang.com/server/java/2693.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

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

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