java 21虚拟线程未改变aqs结构,但使unsafe.park变为协作式暂停:jvm卸载虚拟线程并保存栈快照,由loom调度器管理调度,park/unpark开销趋近方法调用,公平锁排队语义保留但等待延迟降至微秒级。

Java 21 的虚拟线程(Virtual Threads)并未改变 AQS(AbstractQueuedSynchronizer)的核心设计,但显著影响了其锁排队行为与 Unsafe.park 的实际语义——关键在于:虚拟线程调用 park 时,JVM 不再挂起 OS 线程,而是卸载整个虚拟线程(yield + 栈快照保存),交还调度权给 Loom 调度器。
虚拟线程下 AQS 队列结构未变,但节点“阻塞”含义已变
AQS 的 CLH 双向队列(head/tail + Node.waitStatus)在 Java 21 中依然完整保留。区别在于:
- 当虚拟线程在 AQS 中尝试获取锁失败并进入
acquireQueued流程后,若需阻塞,它调用的是LockSupport.park(this),最终触发Unsafe.park(false, 0) - 此时 JVM 不会调用操作系统级的
pthread_cond_wait或WaitForSingleObject,而是由 Loom 运行时接管:保存当前虚拟线程的 Java 栈(压缩为栈帧快照)、清除其在线程本地存储(TLS)和寄存器中的上下文,并将该虚拟线程标记为PARKED状态 - 对应 AQS Node 的
waitStatus = Node.SIGNAL仍存在,但该节点不再绑定一个被 OS 挂起的线程,而是一个可被快速恢复的轻量协程上下文
Unsafe.park 在虚拟线程中是“协作式暂停”,非系统调用
Java 21 中 Unsafe.park 对虚拟线程的实现已从 os::park 切换为 VMThread::park_virtual_thread(HotSpot 内部):
- 底层不进入内核态,无上下文切换开销,也不消耗 OS 线程资源
- 唤醒时(如其他线程调用
unpark或条件满足),Loom 调度器将该虚拟线程重新入队到 carrier thread(载体线程)的执行队列,恢复其栈并继续执行后续逻辑(如重试 CAS 获取锁) - 这意味着 AQS 中常见的“自旋 + park”优化(如
shouldParkAfterFailedAcquire)依然生效,但 park 成本趋近于一次方法调用+对象状态变更,而非传统阻塞
公平/非公平锁行为差异缩小,但排队语义仍保留
即使使用 ReentrantLock(true)(公平锁),虚拟线程的排队逻辑不变:新线程仍需检查队列是否为空、前驱是否为 head,再决定是否入队。但实际效果有变化:
- 由于 park/unpark 极快,且 carrier thread 可高并发调度成百上千虚拟线程,“插队”成本大幅降低,非公平锁的吞吐优势减弱
- 公平性保障仍在 JVM 层维护(AQS 的
hasQueuedPredecessors()仍被调用),只是“等待时间”的物理意义从“毫秒级 OS 调度延迟”变为“微秒级协程调度延迟” - 监控工具(如
jstack -l)中看到的java.lang.VirtualThread$VThreadContinuation#park状态,反映的是 Loom 的逻辑状态,不是 OS 线程状态
开发者无需重写 AQS 子类,但需警惕隐式阻塞迁移
所有基于 AQS 的标准锁(ReentrantLock、Semaphore、CountDownLatch)在虚拟线程中开箱即用,但要注意:
- 不要在虚拟线程中调用会触发
park的阻塞 I/O(如传统SocketInputStream.read()),这会导致载体线程被真正阻塞——应改用 NIO +CompletableFuture或VirtualThread.unmount()显式移交 - AQS 的
tryAcquire等钩子方法若包含耗时计算或同步块,仍可能延长虚拟线程占用 carrier thread 的时间,影响调度效率 - 调试时,
jcmd <pid> VM.native_memory summary</pid>中的线程内存占比会显著下降,因虚拟线程栈默认仅分配 KB 级堆内存,而非 MB 级 OS 栈
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











