虚拟线程

JDK21 之前的多线程模型

在 JDK21 之前,Java 的多线程全部依赖于平台线程(即操作系统线程),JVM 直接将线程的创建与调度全部交给操作系统内核负责。主要调度机制包括:

  • 抢占式调度:高优先级线程可随时中断低优先级线程。
  • 时间片轮转:同一优先级的线程以时间片为单位轮流执行,时间片用完主动让出 CPU。
  • 多级反馈队列:IO密集型线程优先级会被提升,CPU密集型则可能被降低,有助于提高系统整体吞吐。
  • 中断机制:通过线程中断响应外部事件或资源变更。

但传统平台线程模型存在如下弊端:

  • 上下文切换开销大:线程切换需要在用户态与内核态间频繁转换,并保存/恢复寄存器、堆栈等,极大消耗了性能。
  • 资源受限:受限于内存和 CPU 资源,操作系统可创建的线程数量有限。大量线程挂起/恢复会显著降低 CPU 性能。

多线程的核心思想

IO密集型任务理论上可支持远超 CPU 核心数的线程。以 10 核 CPU 跑 Tomcat 为例:

  • 10核 CPU 实际只能物理并行 10 个线程。
  • 大多数请求线程实际长时间被阻塞在 IO 上(如数据库、Redis、文件)。
  • 阻塞时,线程会被 OS 挂起,CPU 能做别的工作。
  • 所以 Tomcat 默认配置 200 线程甚至以上是合理的——关键在于线程大部分都是等 IO。

线程的休眠和唤醒机制

  • 当线程因 IO 需要等待数据时,不应再占用 CPU,因此会进入睡眠(阻塞)状态。此时,调度器不会将不可执行(阻塞)的线程放入运行队列。
  • 睡眠时,内核会将线程移出可执行队列(例如红黑树),加入等待队列。被唤醒时,再重新移回可执行队列,标记为“可运行”。

IO密集型 vs CPU密集型

  • IO密集型:线程大部分时间在等待 IO,处于阻塞、睡眠或等待状态。典型如 Web GUI,用户输入/网络等 IO 事件,空闲时也会等事件。
  • CPU密集型:大量时间用于计算,如复杂科学运算、死循环模拟等。此类线程建议降低优先级,避免“贪占”主机资源。

线程数的选择

  • 线程池过大,可能引发频繁挂起/切换,影响系统性能。
  • 线程池过小,则可能导致 CPU 没被充分利用。
  • 对于高并发 IO 密集场景,需要更灵活的线程模型——虚拟线程应运而生(见下节)。

内核态与用户态

线程/进程平常运行在用户态,仅在需要访问关键资源或做系统调用时切换到内核态(称“陷入内核”)。

  • 用户态:只能访问本进程内的资源,安全但受限。
  • 内核态:可获取全系统资源(磁盘、网络等),拥有最高权限。

哪些场景必须进入内核态?

  1. 进程/线程调度切换
  2. 系统调用:文件操作、网络数据、申请资源(如扩堆、新建线程)。
    • 例如 FileInputStream.read()、线程阻塞/恢复。
  3. 时间片耗尽:判断并切换到其他线程。

为什么调度必须内核态?

只有内核才能安全保存现场、切换栈指针,并了解/控制所有进程状态。否则用户态进程之间互不可见,无法实现可靠切换。


JDK21 之后的多线程革命:虚拟线程

虚拟线程简介

Java 21 引入虚拟线程(Virtual Thread),将线程调度从 OS“用户态化”,由 JVM 负责线程的创建、挂起与切换,实现高并发、轻量级线程。

场景 平台线程 虚拟线程
sleep 陷入内核,挂起/等待唤醒 用户态调度,直接切换下一个虚拟线程
阻塞 I/O 陷入内核,线程挂起 用户态处理,底层常结合 Netty 等异步模型
线程切换 内核态切换 全部用户态完成,极大减少切换损耗

协作式用户态调度

  • 不依赖操作系统的线程调度,完全由 JVM 管理。
  • 极大降低线程开销和上下文切换代价。
  • 适合高并发、大量短生命周期任务。

Go 协程与调度模型全解析

什么是协程(goroutine)

Go 语言内置goroutine,比 OS 线程更轻量的用户态线程,“百万级”并发推进器。通过 Go runtime 管理 goroutine 的创建、挂起、恢复和销毁,适合大规模高并发场景。

GMP 调度模型详解

  • G(Goroutine):代表需调度执行的协程。
  • M(Machine):底层 OS 线程。
  • P(Processor):虚拟的调度器逻辑核,维护本地 goroutine 队列及分配执行。
  • P 的数量(GOMAXPROCS)控制物理并发度,各 P 独立调度、减少锁争用。

GMP 模型调度流程

  1. 每个 P 绑定一个 M,P 维护本地 goroutine 队列。
  2. M (OS 线程) 必须占有 P 才能调度 G 执行。
  3. G 交由所属 P 分配调度,最终运行在 M 上。
  4. 全部切换和调度都发生在用户态,无需频繁进入内核。

目标:最大化利用 M(OS 线程)资源、高效流转大量 G。

协作式与抢占式调度

  • 协作式调度(Go 1.13 及以前):只有在 goroutine 主动让出、函数调用、IO 挂起等”安全点”才调度。死循环/大计算任务可能导致“饿死”其他协程。
  • 抢占式调度(Go 1.14 起):goroutine 占用 CPU 超过一定时长(如 10ms)会被强制挂起,让权给其它协程,调度公平性与系统响应极大提升。

与 Java 虚拟线程比较

  • Java 虚拟线程同样为协作式调度,JVM 用户态实现。
  • Go 基于 GMP 架构支持多核并行,且抢占更早成熟,调度/自稳健性较好。

示例对比

以 10 核心为例:

  • 10 个死循环的虚拟线程或 goroutine(Go 1.13-)将“饿死”第 11 个新加入者,得不到调度机会。
  • Go 1.14+ 强制抢占,哪怕已有高负载死循环,新增协程也能公平调度。
1
2
3
4
// 死循环协程示例
for {
// ... do something
}

小结

Go 协程(goroutine)与 GMP 的组合,让 Go 在超大并发场景下极高性价比地调度和管理线程。协作+抢占调度结合,既快又稳。Java 虚拟线程有同样的理念,Go 在多核、高并发与调度器健壮性上经验更为丰富。


虚拟线程的优势与亮点

  1. 极低开销:全部用户态分配/销毁,远低于传统平台线程。
  2. 无需池化:虚拟线程可以“用多少开多少”,无需固定池,减少参数调优及 OOM 风险。
  3. 高并发友好:切换和唤醒均由 JVM 用户态管理,非常适合短小/高频切换的高并发任务。
  4. 透明兼容:与平台线程 API 相同,原有多线程代码易适配迁移。

扩展与补充

Python 的 GIL(全局解释器锁)

CPython 解释器用 GIL 保证同一时间只有一个线程执行字节码,简化了内存管理和线程安全,但严重影响了多核 CPU 下的线程并发性能。因此 Python 高并发常采用多进程/异步(如 asyncio)以缓解 GIL 限制。

Spring Cloud Gateway 与 WebFlux

  • Spring WebFlux:响应式异步模型,结合 Reactor/Netty 用非阻塞 IO,不再为每个连接分配独立线程,从而极大减少线程/上下文切换的内核开销。
    • Servlet 模型:线程池式,每个请求独占线程,遇 IO 阻塞则线程挂起无法处理其他请求,高并发下资源浪费严重。
    • WebFlux 模型:固定数量事件循环(一般 = CPU 核数),IO 操作注册回调立即释放线程,极大提高资源复用,适合大量并发连接。

总结 & 发展趋势

现代高并发/大规模并行编程的趋势:

  • 线程用户态化:把线程(或协程)的管理从 OS 下沉到运行时(JVM、Go runtime),极大减少切换开销,增强弹性。
  • 调度混合化:协作式+抢占式调度,既高吞吐又更公平,防止“饿死”。
  • 更细粒度线程/协程:允许应用根据业务特点灵活选择。
  • 代表技术:Java 虚拟线程(Project Loom)、Go goroutine、C# async/await、Python asyncio。

一句话总结
高并发范式正从“粗粒度操作系统线程”向“轻量级用户态线程/协程”演化,虚拟线程和协程正是最佳实践的典范。把握此趋势,是现代后端/并发编程的关键能力。


虚拟线程
https://yicizhang00.github.io/posts/编程语言/Java/并发/虚拟线程/
作者
Yici Zhang
发布于
2026年1月6日
许可协议