内容提要
Java 21虚拟线程在同步锁或本地方法栈帧阻塞时可能发生Pinning,导致CPU占用低但吞吐量卡死。JDK 24通过JEP 491修复了synchronized固定问题,但本地栈帧仍存在。检测可用JFR事件或线程转储,修复建议改用ReentrantLock、升级JDK或避免持锁阻塞。
延伸解读
Pinning的识别与检测
Pinning发生时,应用无报错、无警告,CPU占用低但吞吐量卡死,容易被误判为死锁或外部依赖问题。检测手段包括:JDK 21-23使用`-Djdk.tracePinnedThreads=full`打印固定栈帧;JDK 21+使用JFR事件`jdk.VirtualThreadPinned`,注意默认20毫秒阈值;线程转储需用`jcmd Thread.dump_to_file`,观察CarrierThreads载体线程是否被阻塞且挂载虚拟线程。
JDK 24修复的边界
JDK 24的JEP 491解决了synchronized块内的Pinning,但本地方法栈帧(如JNI、FFM、类初始化)导致的固定依然存在。因此,升级到JDK 24并非万能,若代码涉及本地调用或静态初始化器中的阻塞,仍需警惕。同时,JDK 24移除了`-Djdk.tracePinnedThreads`,检测需依赖JFR事件。
修复策略的权衡
将synchronized替换为ReentrantLock是通用修复,但需注意避免在锁内执行阻塞调用,否则仍会串行化。升级JDK 24可消除synchronized固定,但本地栈帧固定仍在。结构性调整(如先查缓存再锁更新)能减少锁持有时间,但需注意ConcurrentHashMap.computeIfAbsent在JDK 21-23上仍可能固定,应避免在映射函数中阻塞。
Q&A
什么是虚拟线程固定(Pinning)?
虚拟线程固定是指虚拟线程在阻塞时无法从载体线程上卸载,导致载体线程被占用,无法执行其他虚拟线程,从而可能引发调度器饥饿,表现为CPU占用低但吞吐量卡死。
虚拟线程固定(Pinning)的触发条件有哪些?
在JDK 21到23中,虚拟线程在synchronized块或方法中阻塞(包括调用Object.wait())时会固定;另外,当虚拟线程的调用栈上存在本地方法栈帧(如JNI、FFM调用或类初始化)并阻塞时也会固定。
JDK 24如何修复虚拟线程固定问题?
JDK 24通过JEP 491修复了synchronized导致的固定问题,它重写了监视器所有权的实现,使监视器与虚拟线程绑定而非载体线程,从而允许虚拟线程在synchronized内部阻塞时卸载。但本地栈帧导致的固定仍然存在。
如何检测虚拟线程固定(Pinning)?
检测方法包括:在JDK 21到23使用-Djdk.tracePinnedThreads=full参数打印固定栈轨迹;使用JFR事件jdk.VirtualThreadPinned(默认开启,但阈值20ms,可调低);使用jcmd Thread.dump_to_file生成线程转储,检查载体线程状态。
如何修复虚拟线程固定(Pinning)问题?
修复方法有三种:将synchronized替换为ReentrantLock;升级到JDK 24及以上;避免在跨外部调用时持有锁,即结构性调整,如先计算值再锁住Map更新。
为什么在JDK 21到23中,ConcurrentHashMap.computeIfAbsent也可能导致虚拟线程固定?
因为ConcurrentHashMap.computeIfAbsent会在内部桶锁下执行映射函数,如果映射函数中发生阻塞,虚拟线程会持有监视器阻塞,从而固定载体线程。
JDK 24中,虚拟线程固定还有哪些情况存在?
在JDK 24中,synchronized固定已修复,但本地栈帧固定仍然存在,包括JNI、FFM调用和类初始化时的阻塞。