现代JIT编译器采用何种策略避免破坏自旋锁?
Java并发可见性问题与JIT编译器自旋锁优化策略
先看《Java并发编程实战》中的经典示例代码:
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }
这段代码可能出现两类错误:
- ReaderThread输出0;
- ReaderThread陷入无限循环。
错误原因分析
处理器不会修改缓存一致性协议的延迟速度,因此volatile关键字的核心作用是禁止指令重排序:
- 输出0是由于指令重排序导致的,主线程中
number = 42和ready = true的执行顺序被打乱,ReaderThread先读取到ready = true,但此时number还未赋值为42; - 无限循环并非缓存一致性问题导致,而是JIT编译器重排序与处理器运行时重排序共同作用的结果。
JIT编译器和处理器遵循volatile非重排序语义的核心是:保持volatile指令的位置固定,其上下的指令可重排、省略或消除,但不会跨越volatile指令。
ReaderThread出现无限循环的根源在于JIT的单线程优化:如果自旋锁内加载的变量在当前线程生命周期内没有被修改的迹象,JIT会将该变量提升到循环外(缓存到寄存器中),导致线程无法读取到其他线程对变量的修改。这是因为线程内一致性规则让JIT仅关注单线程的程序执行顺序(Program Order),无法感知其他线程的修改操作。
在VarHandle相关实践中,解决方案是在自旋内使用getOpaque方法——getOpaque本身没有特殊语义,仅为兼容旧版JIT,让其将该操作视为volatile,从而避免破坏自旋锁的逻辑。
对现代JIT编译器策略的推测
基于上述分析,推测现代JIT编译器可能采用以下两种策略避免破坏自旋锁:
- 在线程内重排序前,对整个程序进行全局分析,识别变量是否可能被其他线程修改;
- 对自旋锁内的加载指令进行特殊的重新组织或重排序处理。
另外,C/C++中未出现此类问题,而C#存在类似问题,由此推测处理器重排序本身不会破坏自旋锁。
技术问询
现代JIT编译器具体采用何种策略避免破坏自旋锁?若上述理解存在错误,欢迎指正。
内容的提问来源于stack exchange,提问作者Delark
相关产品推荐
相关产品推荐

