Skip to content

虚拟线程

约 1181 字大约 4 分钟

2026-03-19

虚拟线程于 Java21 正式引入,以提高高并发下的性能

线程的定义

线程本质上是一段执行指令流及其所需的上下文

平台线程

在虚拟线程之前,Java 中的java.long.Thread默认与 OS 的线程一一对应

  • 昂贵的代价:因为与 OS 的线程一一对应,线程的创建、调度和销毁必须进入内核态
  • 阻塞瓶颈:当线程遇到 I/O 操作发生阻塞,OS 线程也会停摆,且 OS 线程数量有限,实际上浪费了 CPU 资源

虚拟线程

M:N映射 OS 线程,意味着线程的调用与真实的执行实现解耦

  • 轻量级对象:虚拟线程的调用栈不绑定于 OS 的固定内存上,而是动态的存储于 JVM 的堆内存中
  • 充分利用 CPU 资源:当虚拟线程遇到 I/O 阻塞时,JVM 会将上下文保存至堆区并将其从 OS 的线程上卸载,此时该 OS 线程便能够加载其他就绪的虚拟线程

为什么虚拟线程更高效?

回到虚拟线程相较于平台线程最大的特点:多个虚拟线程复用少量载体线程。仅仅是复用线程,在这之前的线程池(如 Java 中的ThreadPoolExecutor)早已经可以实现,通过减少线程的创建和销毁频率以提高性能 对于传统的线程复用,当我们提交某个任务时,实质上是将该任务对象放入一个阻塞队列中,等待空闲线程从中取出并执行其run()方法,但重点在于一旦一个线程执行一个任务,那么就必须一口气执行完成而不能中途切换执行其他任务,这也就意味着当遇到 I/O 阻塞时该线程所分到的时间片就被浪费了 虚拟线程下 JVM 将预测当前调用,若当前调用是「阻塞」的,那么 JVM 将会把对当前虚拟线程进行卸载装载其他虚拟线程,保证载体线程持续处于工作状态

载体线程(Carrier Thread):负责执行虚拟线程中任务的平台线程,虚拟线程通过映射至该线程上运行

卸载:因为线程所执行的指令存放于栈中,JVM通过将当前 OS 线程的栈帧数据复制到堆空间中(StackChunk),并将当前的 PC 记录于虚拟线程的Continuation对象中 装载:当某个虚拟线程的 I/O 完成后,JVM 将会为其分配一个空闲的载体线程,并恢复其状态。将先前存储在堆内存的栈帧拷贝回当前 OS 线程的执行栈上,并重新设置 PC 寄存器

NIO 的支持

为了实现虚拟线程在执行 I/O 等阻塞操作时切换虚拟线程,JDK 21 重写了标准库中网络、I/O 与并发 API,当代码中调用这些接口时,实质上调用的是非阻塞 I/O,无需一直等待 I/O 结果

应用场景

I/O 密集型

虚拟线程最大的优点是不必因为 I/O 阻塞拖慢线程效率,在 I/O 密集型任务中,假设有大量网络请求需要查询数据库,将每一个请求作为一个虚拟线程,当遇到阻塞时就会自动卸载,处理其他的请求 在以下的例子中创建 10,000 个虚拟线程,每个线程模拟 1 秒的 I/O 阻塞(Thread.sleep在 JDK 21 下同样为非阻塞),可以看到最终的运行时长仅为 1.065 秒。仅通过卸载装载的一定代价换取无需等待 I/0 的效率 image

CPU 密集型

既然虚拟线程有无视 I/O 阻塞的特性,目的是保证 CPU 的利用率,那么 CPU 密集型甚至不需要考虑阻塞,那么是否无脑使用虚拟线程就好了? 让我们回到虚拟线程的工作方式:区别与传统的平台线程,JVM 默认根据宿主机的 CPU 核数创建同等数量的载体线程,而虚拟线程与载体线程通过映射实现线程的执行,那么在 8 核的 CPU 上意味着最多 8 个虚拟线程同时运行。 当虚拟线程遇到阻塞时才会让出,而 CPU 密集型下(如视频编码),其他的虚拟线程必须等待已经绑定载体线程的虚拟线程执行完成,假如此时出现其他的