← 返回题目列表

Java 虚拟线程是什么?适合什么场景?

高频 中等 第 7 / 24 题 更新于 2026/07/26
虚拟线程Project LoomJava 21pinning

简化版

虚拟线程是 JDK 调度的轻量 Thread,在 Java 21 正式发布。它让大量主要在等待网络、数据库等 I/O 的任务继续使用清晰的“一任务一线程”代码,但它不会加速 CPU 密集计算,也不会扩大数据库连接池等下游容量。

详细版

平台线程在运行期间一直占用对应的操作系统线程;虚拟线程由 JDK 调度,运行时被挂载到平台载体线程上。当 JDK 可识别的阻塞 I/O 发生时,虚拟线程可以卸载,释放载体去运行其他任务,就绪后再挂载继续执行。

使用时应把握四点:

  • 虚拟线程廉价但不是免费,数量仍受内存、任务状态和外部资源限制。
  • 应按任务创建虚拟线程,不要像平台线程那样建固定大小的虚拟线程池。
  • 需要限制数据库连接或外部 API 并发度时,用连接池、Semaphore 或限流器限制资源,不是通过池化虚拟线程实现。
  • JDK 21–23 中,在 synchronized 内阻塞可能造成 pinning;JDK 24 的 JEP 491 已消除了这类常见 pinning,不应再笼统要求把 synchronized 全部换成 ReentrantLock

完整版教学

一、它要解决的是吞吐量与代码复杂度

传统服务端如果为每个请求长时间保留一个平台线程,当大量请求在等待下游 I/O 时,操作系统线程会成为稀缺资源。异步回调或响应式模型可以减少线程占用,但会将顺序业务逻辑改写成不同的组合风格。

虚拟线程保留了顺序调用、异常传播和线程堆栈的直观性,同时避免一个阻塞任务长期独占一个操作系统线程。它主要提高高并发 I/O 型应用的吞吐量上限,不是让单个请求的网络延迟变短。

二、挂载、卸载与载体线程

虚拟线程要执行 Java 代码时,会被 JDK 调度到某个平台线程上,这个平台线程称为载体。当虚拟线程在支持的阻塞点等待时,JDK 保存它的执行状态并卸载,载体随后可运行另一个虚拟线程。

这是 M:N 调度:大量虚拟线程复用较少的平台线程。同一时刻能够真正并行执行 Java 代码的任务数仍受 CPU 核数和载体调度限制,因此纯计算任务不会因为换成虚拟线程就自动加速。

三、正确的创建方式

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<User> user = executor.submit(() -> loadUser());
    Future<List<Order>> orders = executor.submit(() -> loadOrders());

    render(user.get(), orders.get());
}

newVirtualThreadPerTaskExecutor() 为每个提交任务创建新虚拟线程,它不是固定数量的线程池。也可使用 Thread.startVirtualThread(runnable)Thread.ofVirtual() 创建虚拟线程。

不要为了“保护数据库”而把虚拟线程数固定成 20。数据库容量是另一个资源维度,应由连接池或信号量控制,让其他不使用数据库的虚拟线程仍可继续运行。

四、pinning 要按 JDK 版本理解

pinning 指虚拟线程阻塞时无法从载体卸载,使载体也被占住。在 JDK 21–23 中,虚拟线程在 synchronized 方法或代码块内发生阻塞时可能 pinning,因此旧资料常建议将长时间阻塞路径上的 synchronized 换成 ReentrantLock

JDK 24 交付的 JEP 491 改造了 monitor 实现,虚拟线程现在可以在持有 synchronized monitor、等待 monitor 或执行 Object.wait() 时卸载,这类 pinning 已不再是迁移锁实现的理由。仍有一些与 native frame、类加载或类初始化有关的少见 pinning 情况,应使用 JFR 等工具基于现象诊断,不要继续套用旧版本规则。

五、虚拟线程不会消除资源治理

如果同时启动大量任务,每个任务都保存大对象、ThreadLocal 数据或未完成请求上下文,总内存仍会增长。虚拟线程支持 ThreadLocal,但在高基数线程上放置大型或可变的 ThreadLocal 状态需要格外谨慎。

同样,当所有任务都在等待一个小型连接池时,虚拟线程只是使等待成本更低,并没有创造新连接。系统仍需要超时、取消、限流、背压、连接池和服务降级。

六、用数字区分并发、并行与容量

假设有 10,000 个请求,每个请求做 2 ms CPU 计算,再等待下游 198 ms。单个请求仍约 200 ms,虚拟线程不会把它变成 20 ms;它的价值是等待期间卸载载体,让少量平台线程继续运行其他就绪任务。若 10,000 个请求同时都要数据库连接,而连接池只有 100,最多仍只有约 100 个查询占用连接。

虚拟线程数量:可很大,表示并发任务数
载体/CPU 核数:限制同一时刻执行 Java 计算的并行度
数据库连接数:限制同一时刻访问数据库的任务数
任务类型虚拟线程收益原因
大量网络/JDBC 等待通常明显等待时可让出载体
8 核上的纯 CPU 循环不会突破 8 核并行度仍受 CPU 限制
极短小任务需测量调度也有成本
依赖 100 个连接仍受 100 限制下游容量没有增加

七、取消、ThreadLocal 与可观测性

虚拟线程仍是 Thread,支持中断和 ThreadLocal,但中断只是协作取消信号,业务代码、驱动和客户端必须正确响应。若每个虚拟线程保存 1 MiB ThreadLocal 数据,10,000 个并发任务的理论数据规模就是约 10 GiB,因此“线程便宜”不等于“每线程状态免费”。

不要通过固定大小虚拟线程池做限流,应在稀缺资源处使用 Semaphore、连接池、速率限制和超时。排障时使用能展示虚拟线程的新式 thread dump、JFR 事件及应用指标,不能只按平台线程数量判断负载。

八、JDK 版本迁移表

JDK虚拟线程状态synchronized pinning 重点
19第一次预览可能 pin
20第二次预览可能 pin
21–23JEP 444 正式monitor 内阻塞可能 pin
24+JEP 491synchronized/monitor wait 不再因该原因 pin

JDK 24 后应根据互斥语义选择 synchronized 或 Lock,而不是为了虚拟线程机械替换。仍可能存在 native frame、类加载或类初始化相关的少见 pinning,应通过证据诊断;旧的 jdk.tracePinnedThreads 在 JEP 491 后不再需要并被移除作用。

记忆钩子:虚拟线程让“等待便宜”,CPU、内存和下游资源仍然昂贵;JDK 24 又把 synchronized pinning 从常见问题变成了旧版本知识点。

九、常见误区与追问

  • 误区:虚拟线程会让单次 I/O 响应更快。 它主要提高大量等待任务的可扩展性,不会缩短网络或数据库本身的延迟。
  • 误区:虚拟线程应该放进固定大小线程池反复复用。 应按任务创建,稀缺资源另行限流。
  • 误区:虚拟线程可以让 CPU 密集任务无限并行。 同一时刻执行计算仍受 CPU 核数和调度限制。
  • 误区:所有 JDK 版本中的 synchronized 都会 pin 虚拟线程。 这是 JDK 21–23 的重要边界,JDK 24 的 JEP 491 已解决 monitor 相关常见 pinning。
  • 追问:连接池只有 100 个连接时如何限制并发? 继续依赖连接池、Semaphore、超时和背压,而不是限制虚拟线程总数。
  • 追问:虚拟线程能否使用 ThreadLocal? Java 21 正式版支持,但海量线程上的大对象 ThreadLocal 会放大内存成本。
  • 追问:何时仍适合 CompletableFuture 或事件循环? 已有异步 API、明确数据流编排、精细背压或成熟事件驱动框架时仍可能更合适。

十、加强记忆

虚拟线程的价值是让大量 I/O 等待任务使用简洁的一任务一线程模型:运行时挂载到载体,支持的阻塞点卸载让出载体。它不加速 CPU 计算,不应被池化,也不会突破下游容量;而 synchronized 造成常见 pinning 是 JDK 21–23 的版本边界,JDK 24 起已由 JEP 491 基本消除。