synchronized 会让虚拟线程卡死在平台线程上,因其依赖JVM对象头中绑定平台线程指针的monitor机制,导致虚拟线程无法卸载;ReentrantLock可替代但需避免嵌套synchronized或使用await();JFR的jdk.virtualThreadPinned事件可快速定位固定点。

为什么 synchronized 会让虚拟线程“卡死”在平台线程上
因为 synchronized 是 JVM 级的阻塞原语,它依赖对象头中的锁状态(比如 monitor 的 owner 字段直接存的是 JavaThread* 指针),而这个指针绑定的是真实平台线程。虚拟线程一旦进入 synchronized 块,JVM 就无法安全地把它从当前载体线程上卸载——否则 monitor 的 owner 就会指向一个已不存在或正在切换的线程上下文。
这不是 bug,是设计限制:Project Loom 明确将 synchronized、本地方法调用、Object.wait() 等列为“固定点”(pinning points)。只要虚拟线程停在这里,它的载体线程就只能干等,没法去跑别的虚拟线程。
用 ReentrantLock 替代 synchronized 能解决问题吗
能,但要注意用法。默认的 ReentrantLock 是公平锁还是非公平锁不关键,关键是它不触发 pinning——因为它底层靠 Unsafe.park()/unpark() 实现,而这两个操作是 Loom-aware 的,允许 JVM 在挂起时卸载虚拟线程。
- ✅ 正确用法:
lock.lock()+try/finally保证释放,或使用lock.tryLock()避免无限等待 - ❌ 错误用法:在
lock.lock()后又嵌套synchronized块,等于白换 - ⚠️ 注意:如果用了
lock.newCondition().await(),这仍然会 pin!因为await()底层调用Object.wait()
怎么快速发现代码里有虚拟线程被固定了
开 JDK Flight Recorder(JFR),抓 jdk.virtualThreadPinned 事件——这是最直接的信号。它会在每次虚拟线程因 synchronized 或其他固定点被钉住时发出一条记录,附带堆栈和持续时间。
立即学习“Java免费学习笔记(深入)”;
- 启动命令示例:
java -XX:StartFlightRecording=duration=60s,filename=recording.jfr,settings=profile -jar app.jar - 分析时重点看事件频次和平均耗时:1 秒内出现上千次
virtualThreadPinned,基本说明你在热点路径上用了synchronized - 生产环境建议常驻低开销采样:
-XX:FlightRecorderOptions=defaultrecording=true,setting=profile
Spring Boot 3.2+ 帮你绕开了多少坑
它不是“自动修复” synchronized,而是把大量可能踩坑的地方做了无锁化或异步适配:比如 @Transactional 默认不再用同步锁管理事务资源;WebMvc 的 HandlerAdapter 内部改用 ReentrantLock 或 StampedLock;甚至 SimpleAsyncTaskExecutor 在虚拟线程模式下会被静默替换为 VirtualThreadTaskExecutor。
- 但你的业务代码里写的
synchronized依然照 pin 不误 - 升级后仍需 grep 全项目:
grep -r "synchronized" src/main/java/,逐个评估是否可删或改写 - 特别注意工具类、计数器、缓存包装器——这些地方最容易偷偷写个
synchronized(this)
synchronized 卡住,它就退化成一个占着茅坑还拉不出的平台线程。真正的难点不在替换语法,而在识别那些你以为“只是读个共享变量”的隐式竞争点。










