Java 21 虚拟线程深度解析:高并发编程的游戏规则改变了

举报
yd_212140359 发表于 2026/10/21 22:47:13 2026/10/21
【摘要】 前言长期以来,Java 开发者面对高并发场景时,一直被一个难题困扰:线程太贵了。传统 Java 线程是操作系统线程(Platform Thread)的直接封装,一个线程默认占用 1MB 左右的栈内存。当你需要处理 10 万个并发请求时,创建 10 万个 OS 线程不仅内存扛不住,上下文切换的开销也会让 CPU 苦不堪言。这就是为什么过去几年响应式编程(WebFlux、RxJava)和异步框...

前言

长期以来,Java 开发者面对高并发场景时,一直被一个难题困扰:线程太贵了。

传统 Java 线程是操作系统线程(Platform Thread)的直接封装,一个线程默认占用 1MB 左右的栈内存。当你需要处理 10 万个并发请求时,创建 10 万个 OS 线程不仅内存扛不住,上下文切换的开销也会让 CPU 苦不堪言。

这就是为什么过去几年响应式编程(WebFlux、RxJava)和异步框架(Netty)如此流行——它们本质上都是在绕开"线程太贵"这个问题,代价是代码变得复杂、调试变得困难。

而 Java 21 正式发布的虚拟线程(Virtual Threads,JEP 444),终于从 JVM 层面正面解决了这个问题。

一、虚拟线程是什么?

一句话概括:虚拟线程是由 JVM 而非操作系统调度的轻量级线程。

它的核心机制是:

  • 多个虚拟线程在任意时刻会被挂载(mount)到少量 carrier thread(载体线程,即 OS 线程)上运行;
  • 当虚拟线程遇到阻塞操作(如 IO 读取、sleep)时,JVM 会把它从载体线程上卸载(unmount),把载体线程让给其他虚拟线程;
  • 阻塞结束后,虚拟线程会被重新调度到某个载体线程上继续执行。
传统模型:1 个 Java 线程 = 1 个 OS 线程(昂贵)
虚拟线程:N 个虚拟线程 → 复用 M 个 OS 线程(N >> M)

这意味着你可以用同步阻塞的代码风格,获得异步非阻塞的吞吐量。

二、快速上手

1. 创建虚拟线程

// 方式一:直接启动
Thread.startVirtualThread(() -> {
    System.out.println("运行在: " + Thread.currentThread());
});

// 方式二:使用 Builder
Thread vThread = Thread.ofVirtual()
        .name("my-virtual-thread")
        .start(() -> handleRequest());

// 方式三:Executors(最重要用法)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100_000; i++) {
        final int id = i;
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1)); // 模拟 IO 阻塞
            return id;
        });
    }
} // executor 会等待所有任务完成

注意最后这个例子:10 万个任务,每个都阻塞 1 秒,总耗时约 1 秒。换成平台线程池,这是不可想象的。

2. Spring Boot 3.2+ 中启用

只需一行配置:

spring:
  threads:
    virtual:
      enabled: true

启用后,每个 Web 请求都会在一个独立的虚拟线程中处理。你的 Controller 代码一行都不用改,但并发能力大幅提升。

三、虚拟线程 vs 平台线程 vs 响应式编程

维度 平台线程 虚拟线程 响应式(WebFlux)
调度者 OS JVM Event Loop 线程
内存开销 ~1MB/线程 ~几 KB/线程 极低
编程模型 同步阻塞 同步阻塞 异步非阻塞(Flux/Mono)
调试体验 好 好 差(栈信息破碎)
阻塞代码兼容性 天然支持 天然支持 会打死 Event Loop
学习成本 低 低 高

关键结论:虚拟线程让"一个请求一个线程"的传统同步模型重新变得可行且高效,而不必被迫转向响应式。

四、避坑指南:这些场景不要用虚拟线程

虚拟线程不是银弹,用错了反而适得其反。

❌ 1. CPU 密集型任务

虚拟线程的优势在于应对阻塞,而不是加速计算。CPU 密集型任务(图像处理的、复杂算法、加解密)应该继续使用固定大小的平台线程池(核数 × N),虚拟线程不会带来任何收益。

❌ 2. 用池化虚拟线程

// 错误示范:不要池化虚拟线程!
ExecutorService pool = Executors.newFixedThreadPool(10, Thread.ofVirtual().factory());

虚拟线程本身就是为"每个任务一个线程"设计的,池化它们毫无意义,反而人为限制了并发。正确的用法是 newVirtualThreadPerTaskExecutor。

❌ 3. 滥用 synchronized 导致 Pinning(钉子户问题)

虚拟线程在 synchronized 块内阻塞时,无法从载体线程上卸载,这叫线程钉定(pinning),会导致载体线程饥饿:

// 有问题的写法
synchronized (lock) {
    httpClient.send(request, ...); // 阻塞 IO 在 synchronized 内 → pinning
}

// 推荐改用 ReentrantLock
private static final ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    httpClient.send(request, ...); // 可以正常卸载
} finally {
    lock.unlock();
}

JDK 24 的 JEP 491 已从根本上解决了 synchronized 的 pinning 问题,但如果你运行在 21~23,仍然需要注意。

❌ 4. 无节制地创建 ThreadLocal 大对象

虚拟线程可以轻松创建百万个,如果每个都持有大对象或大量 ThreadLocal 缓存,内存照样会爆。官方建议:虚拟线程不适合与 ThreadLocal 一起使用,必要时用 ScopedValue(JEP 446 预览)替代。

五、工作原理初探:Continuation 与调度器

虚拟线程的底层是 java.lang.VirtualThread,它内部持有一个 Continuation(续体)对象:

  • runContinuation:保存了线程的执行栈(帧数据存放在堆上,而不是固定大小的原生栈),所以它可以按需增长、也可以整体"冻结";
  • scheduler:默认是一个 ForkJoinPool,负责把可运行的虚拟线程提交给载体线程执行。

当虚拟线程调用 Thread.sleep() 或 socket read 时,实际流程是:

1. JVM 保存当前栈帧到堆(unmount)
2. 注册一个定时器/IO 回调
3. 载体线程立刻执行其他虚拟线程
4. 回调触发 → 虚拟线程进入调度队列
5. 某个载体线程重新挂载它,从断点继续执行

这套机制让"阻塞"的成本从"占用一个 OS 线程"降为"堆里多存一份栈数据"。

六、性能实测参考

JVM 官方与社区的基准测试给出了这样一个普遍规律:

  • IO 密集型应用(HTTP 调用、数据库查询):吞吐量提升明显,从几十倍到上百倍都有可能(取决于并发量与 IO 延迟占比);
  • 单请求延迟:基本不变,甚至略有增加——虚拟线程不加速单个请求;
  • 内存占用:百万级并发连接的内存占用显著低于平台线程模型。

一句话:虚拟线程优化的是"并发规模",不是"单任务速度"。

七、未来演进:结构化并发与调度器

Java 21 同时带来了两个配套预览特性:

  1. Structured Concurrency(结构化并发,JEP 453):用 StructuredTaskScope 把一组并发任务当作一个单元管理——任一子任务失败可自动取消其他任务,避免孤儿线程泄漏:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Subtask<String> user  = scope.fork(() -> findUser());
    Subtask<Integer> order = scope.fork(() -> fetchOrder());

    scope.join().throwIfFailed();

    return new Response(user.get(), order.get());
} // scope 关闭时保证所有子任务已结束
  1. Scoped Values(JEP 446):面向虚拟线程时代的 ThreadLocal 替代品,支持不可变作用域值和在虚拟线程间高效继承。

(这两个特性在后续版本中仍在持续迭代,如 JDK 25 中结构化并发已转正为 JEP 505,落地前请查阅对应版本文档。)

总结

  • 虚拟线程把 Java 的高并发门槛拉低了:同步的代码,异步的性能;
  • 它的适用面非常明确:IO 密集、高并发、大量阻塞的场景;
  • CPU 密集型任务、池化思维、synchronized 中的阻塞 IO、ThreadLocal 滥用是四大常见坑;
  • Spring Boot 3.2+ 一行配置即可启用,升级成本低;
  • 结合结构化并发与 Scoped Values,Java 的并发编程模型正在迎来十年来最大的范式升级。

如果你还在为高并发写复杂的异步代码,是时候重新审视"一个请求一个线程"这个最朴素的模型了——它在 Java 21 之后,真的够用了。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。