虚拟线程的挂起与恢复由JVM在字节码层面自动触发,依托Continuation原语保存/还原执行上下文,释放并复用载体线程,全程无需用户干预,对代码完全透明。

虚拟线程的调度不依赖用户手动干预,它的挂起与恢复由 JVM 在字节码层面自动触发,核心支撑就是 Continuation——一个 JVM 内部原语,不暴露为 Java 可见的 API,也不等同于 Kotlin 的 Continuation<T> 接口。
挂起不是“暂停线程”,而是保存执行快照
当虚拟线程执行到阻塞点(如 Thread.sleep()、Object.wait()、同步文件读写、阻塞式 socket 调用),JVM 会:
- 捕获当前栈帧的完整执行上下文(包括局部变量、操作数栈、程序计数器位置)
- 将这些数据序列化为 Continuation 实例,存入堆内存(非线程栈)
- 标记该虚拟线程为“已挂起”,并立即释放它正在使用的载体线程(Carrier Thread)
- 载体线程随即从 ForkJoinPool 或自定义调度器中拉取下一个就绪的虚拟线程继续执行
恢复不是“唤醒线程”,而是重装上下文到新栈
阻塞条件满足后(比如 sleep 时间到、IO 数据就绪),JVM 不是简单地“唤醒”原线程,而是:
- 定位对应的 Continuation 对象
- 在某个可用的载体线程上,为其分配新的轻量级栈空间
- 将之前保存的局部变量、指令位置等全部还原到该栈上
- 从挂起点的下一条字节码继续执行,对用户代码完全透明
哪些操作能触发挂起?哪些不能?
能否挂起,取决于 JVM 是否识别该调用为“可中断的阻塞点”:
-
能触发:所有 JDK 标准阻塞 API,如
Thread.sleep()、LockSupport.park()、Files.readString()(非 NIO)、System.in.read()、传统 JDBC 查询(未启用异步驱动) -
不能触发:纯 CPU 密集型循环(
while(true) { i++; })、本地方法(JNI)中未配合 JVM 挂起协议的阻塞、或调用了被 JVM 标记为@ForceInline且绕过挂起检查的内部方法
一旦发生后者,虚拟线程会长期占用载体线程,造成“pinned”现象,JFR 中会出现 jdk.VirtualThreadPinned 事件告警。
状态表现与调试关键点
虚拟线程对外状态仍显示为 RUNNABLE,Thread.getState() 不会返回 PARKED 等新值——这是为了兼容现有监控工具和诊断逻辑。真正判断是否挂起生效,应看:
-
Thread.currentThread().isVirtual()返回true - JFR 录制中
jdk.VirtualThreadMount和jdk.VirtualThreadUnmount事件成对出现 - 载体线程数稳定(如默认 ForkJoinPool 并行度为 CPU 核数),但活跃虚拟线程数可达数十万
Continuation 机制让上下文切换成本降至纳秒级,代价是每次挂起都涉及堆内存分配与 GC 压力,因此不适合高频短时挂起场景(如微秒级轮询)。

















