虚拟线程采用M:N映射,由JVM调度并动态挂载到少量平台线程(Carrier Thread)上执行;平台线程则与操作系统线程严格1:1绑定,创建成本高、栈大(默认1MB)、数量受限。

虚拟线程和平台线程的底层映射关系不同
平台线程(Platform Thread)是 java.lang.Thread 的传统实现,它与操作系统线程(OS thread)严格一对一绑定。每次调用 new Thread() 或 Thread.ofPlatform().start(),JVM 就会触发一次系统调用,向 OS 申请一个内核线程资源,并为其分配固定大小的栈(默认约 1MB)。这意味着:线程数直接受限于 OS 能创建的线程上限(通常几千到几万),且内存占用刚性、不可压缩。
虚拟线程(Virtual Thread)不直接绑定 OS 线程。它由 JVM 在用户态调度,运行时被动态挂载到少量“载体线程”(Carrier Thread,本质就是平台线程)上执行。当虚拟线程执行阻塞 I/O(如 InputStream.read()、Socket.connect())时,JVM 自动将其卸载(unmount),释放载体线程去跑别的虚拟线程;I/O 完成后,再重新挂载(mount)恢复执行。这种 M:N 调度模型让百万级并发成为可能。
创建方式和 API 行为差异明显
虽然两者都是 Thread 实例,但构造路径完全不同:
-
Thread.ofVirtual()是首选创建入口,返回Thread.Builder.OfVirtual;可链式配置名称、是否守护等,最后用.start()或.unstarted() -
Thread.startVirtualThread(Runnable)是静态快捷方法,适合简单场景 -
new Thread()和Thread.ofPlatform()创建的永远是平台线程,哪怕在 JDK 21+ 也保持原有语义
注意:Thread.currentThread().isVirtual() 可在运行时判断当前线程类型;但 Thread.activeCount() 不统计虚拟线程,只反映平台线程数量。
资源开销与适用场景截然不同
平台线程适合 CPU 密集型任务——因为其栈空间稳定、无调度抖动,且能独占 OS 线程做长时间计算。但一旦用于大量短生命周期或 I/O 等待任务(比如 Web 请求处理),就会迅速耗尽内存和 OS 线程句柄。
虚拟线程恰恰相反:
- 初始栈仅几 KB,按需分段增长(
Segmented Stack),堆上分配,GC 可回收 - 启动延迟约 50 微秒(vs 平台线程 ~1 毫秒),10,000 个并发下内存占用可低至 112 MB(平台线程同负载需 890 MB)
- 对标准阻塞 I/O、
synchronized、Object.wait()等完全透明兼容,无需改写现有库 - 但不适合长期霸占 CPU 的任务——它会被 JVM 强制让出载体线程,导致意外调度延迟
线程本地变量(ThreadLocal)行为有隐含约束
JDK 21 起,虚拟线程强制支持 ThreadLocal,但机制与平台线程不同:它的 ThreadLocal 值存储在虚拟线程对象自身字段中,而非绑定到载体线程。这带来两个关键影响:
- 每次虚拟线程被挂起/恢复,
ThreadLocal值仍保持有效,行为符合直觉 - 但若手动将虚拟线程从一个载体线程迁移到另一个(极少见),
ThreadLocal不会跨载体线程继承——这点和平台线程的“线程绑定”本质一致 - 某些深度依赖线程绑定生命周期的框架(如老版本 HikariCP 的连接泄漏检测逻辑)可能因虚拟线程快速启停而误报,需确认其是否适配 JDK 21+ 的虚拟线程语义
真正容易被忽略的是:虚拟线程的“轻量”不等于“无成本”。它依赖 JVM 的 Continuation 机制和 ForkJoinPool 调度器,如果混用大量自定义 ForkJoinPool 或显式调用 Thread.sleep() 配合错误的超时策略,反而可能引发调度饥饿或 carrier 线程争用——这时候看 jcmd 和 jstack 输出比看文档更管用。











