线程虚拟机的核心,是一套运行在用户态的线程调度与栈管理机制它把创建、调度、销毁线程的控制权从操作系统内核手里接过来,直接决定了一个服务能扛住多少并发、每次切换烧掉多少 CPU 周期、以及在堆里额外背多少内存。 一句话说透:线程模型不是语法选择,而是性能预算的分配方式。
线程虚拟机的核心:调度器、栈与状态机
平台线程时代:虚拟机只是内核的“影分身”
在 JDK 21 之前,JVM 里的 new Thread() 基本是一比一映射到内核线程,虚拟机本身几乎不做调度,创建要走系统调用,切换要陷入内核,栈由操作系统按固定大小分配。
- 栈大小由
-Xss控制,公开文档显示 64 位 Linux 上 HotSpot 默认约 1MB - 每个线程一份独立的栈空间,创建即占用,用完才释放
- 调度权在内核,JVM 只能被动等结果
这意味着什么?一台 8GB 内存的机器,开几千个线程就开始捉襟见肘,线程不再是廉价的“逻辑单位”,而是实打实的系统资源。
虚拟线程时代:把栈搬进堆里
JDK 21 正式引入虚拟线程后,这套逻辑被改写,虚拟线程不直接绑内核线程,而是挂在一组“载体线程”(carrier thread)上运行,典型实现是 ForkJoinPool,默认并行度等于 CPU 核数。
关键变化有三点:
- 栈不再由内核分配,而是以栈块(stack chunk)形式存放在 Java 堆上,按需增长、按需收缩
- 阻塞即卸载:虚拟线程遇到 IO 阻塞时会从载体线程上摘下来,载体线程立刻去跑别的虚拟线程
- 调度权回到用户态,JVM 自己决定谁上谁下
核心组件清单
- 调度器:默认为 ForkJoinPool,可通过系统属性调节并行度
- 栈管理:堆上栈块 + 栈帧复制,mount/unmount 时在两个位置间搬移
- 状态机:NEW、STARTED、RUNNING、PARKED、TERMINATED 等状态流转
- 与 GC 的耦合:栈在堆上,意味着 GC 需要扫描它,这直接影响停顿表现
业内专家指出,虚拟线程的价值不在于“更快”,而在于“更便宜地等待”,对于大量时间花在等数据库、等 RPC、等 HTTP 响应的业务,这是结构性改变。
虚拟线程和传统平台线程性能对比:差在哪
内存开销的差距
平台线程的栈是预留制,1MB 起步,哪怕只用了 20KB,虚拟线程的初始栈块通常只有几百字节到几 KB 量级,且能随调用深度伸缩。
结果就是:同样 1GB 内存预算,平台线程可能只够开千级,虚拟线程可以开到数十万级,这个量级差异,直接决定了架构能不能用“一请求一线程”的简单写法。
上下文切换的代价
- 平台线程切换:用户态 ↔ 内核态往返,保存/恢复完整寄存器上下文,成本在微秒量级
- 虚拟线程切换:纯用户态操作,本质是栈帧在堆上的搬运,成本显著更低
行业共识认为,高并发场景下切换开销往往是隐性瓶颈,线程数一旦超过 CPU 核数几十倍,调度器的时间片竞争会让有效算力明显下降。
关键维度对比
| 维度 | 平台线程 | 虚拟线程 |
|---|---|---|
| 映射关系 | 1:1 内核线程 | M:N 挂载体线程 |
| 默认栈大小 | 约 1MB | 初始几 KB,动态伸缩 |
| 阻塞行为 | 占住内核线程 | 卸载,让出载体 |
| 适用负载 | CPU 密集、少量并发 | IO 密集、海量并发 |
| 监控手段 | jstack、Thread.print | JFR、Thread.dump_to_file |
Java虚拟线程在什么场景下使用最合适
适合:IO 密集、高并发等待
典型画面是这样的:一个网关服务,每个请求要串行调用 3 到 5 个下游接口,单请求耗时 80ms,95% 时间在等网络,用固定线程池,线程数就是并发上限;用虚拟线程,每个任务一条虚拟线程,吞吐由下游容量决定,而不是由池子大小决定。
- Web 服务端请求处理
- 批量调用外部 API 的聚合层
- 消息消费端的并发拉取
- 大量短连接的数据库访问层
不适合:CPU 密集型计算
虚拟线程不会让 CPU 变快,纯计算任务挂上去,照样占满载体线程,还额外多了 mount/unmount 的开销,视频转码、加密运算、复杂规则引擎这类活儿,老老实实用固定大小的平台线程池更稳。
三个高频坑
- synchronized 导致 pinning:早期版本里,虚拟线程在 synchronized 块内阻塞会把它钉在载体线程上,退化成平台线程行为,JDK 24 已通过 JEP 491 解决,老版本需改用
ReentrantLock。 - ThreadLocal 膨胀:每个虚拟线程一份 ThreadLocal 副本,开十万个线程就是十万份数据,堆压力陡增,新代码优先考虑
ScopedValue。 - 池化虚拟线程:虚拟线程本来就该用完即弃,再套一层池子等于把优势还回去。
国内电商大促场景下的线程模型选型
大促流量有个特点:瞬时并发高、单请求链路长、下游依赖多,这种形态下,线程池的排队时间常常比业务处理时间还长。
迁移路径(可验证步骤)
- 先用 JFR 抓一段流量,确认线程阻塞比例:
jcmd <pid> JFR.start duration=60s filename=base.jfr - 把 IO 密集的线程池替换为
Executors.newVirtualThreadPerTaskExecutor() - 检查代码里的 synchronized 块,替换为
ReentrantLock - 用
Thread.ofVirtual().name("vt-", 0).factory()给线程命名,方便排查 - 灰度上线,对比 P99 延迟与 GC 停顿
监控与调优命令
- 导出线程快照:
jcmd <pid> Thread.dump_to_file -format=json /tmp/threads.json - 关注 JFR 事件:
jdk.VirtualThreadPinned、jdk.VirtualThreadStart - 调整调度并行度:
-Djdk.virtualThreadScheduler.parallelism=16
- 大堆场景建议搭配低停顿收集器:
-XX:+UseZGC
从平台线程迁移到虚拟线程的成本高不高
成本主要不在 API 改造,而在依赖排查,大部分业务代码直接就能跑,真正花时间的是找出哪些库在内部用了 synchronized、哪些框架假设了线程池语义。
改造量大致分三档:
- 低:纯 HTTP/RPC 调用,替换执行器即可,通常一两天能跑通
- 中:用了连接池、ThreadLocal 上下文传递,需要逐个核对
- 高:依赖 native 库或大量 synchronized 的存量组件,需要评估 pinning 影响
收益则取决于等待占比,等待占比越高,收益越明显;纯计算服务基本看不到提升,反而多了一层抽象。
Q&A:线程虚拟机核心与系统性能常见问题
虚拟线程会取代线程池吗
不会完全取代,虚拟线程解决的是“等待太贵”的问题,CPU 密集任务仍需要固定大小的线程池来避免过度并行,未来的常见形态是:IO 用虚拟线程,计算用平台线程池,两者分工。
虚拟线程核心参数怎么调
最常动的是调度并行度 jdk.virtualThreadScheduler.parallelism,默认等于 CPU 核数,如果任务里有少量阻塞型 native 调用,可以适度调高,另一个是最大池大小 jdk.virtualThreadScheduler.maxPoolSize,用于兜底突发负载,改之前先用 JFR 看 pinning 频率,别凭感觉调。
线程数开到十万会不会拖垮 GC
会有影响,因为栈块在堆上,GC 需要扫描和移动,数量级越大,对收集器的低停顿能力要求越高,实践路径是先用 ZGC 或 G1 观察停顿曲线,再决定是否给虚拟线程数量设上限。
线程虚拟机核心的演进方向很清晰:把调度的粒度做细,把等待的成本做低。 理解它管什么、省什么、又额外带来什么开销,才能在大促、在高并发、在每一次容量评估里做出不拍脑袋的决定。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/731454.html




