Java多线程中非volatile变量行为复现及缓存读取疑问
复现Java中非volatile变量的可见性问题
看起来你在尝试复现非volatile变量的可见性问题,但发现线程读取变量的行为和预期不符?这大概率是JIT即时编译器的优化在“捣乱”,咱们一步步拆解问题,给出能稳定复现的方案。
先还原你的代码场景
首先整理出你提到的核心代码,方便分析:
OccurrenceCounter.java
public class OccurrenceCounter { // 未使用volatile修饰的变量 private boolean test = false; public boolean isTest() { return test; } public void setTest(boolean test) { this.test = test; } }
ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) throws InterruptedException { OccurrenceCounter occurrenceCounter = new OccurrenceCounter(); // 线程1:循环检查test的值 Thread thread1 = new Thread(() -> { while (!occurrenceCounter.isTest()) { // 空循环是问题的关键! } System.out.println("线程1检测到test变为true"); }); // 线程2:延迟1秒后修改test的值 Thread thread2 = new Thread(() -> { try { Thread.sleep(1000); occurrenceCounter.setTest(true); System.out.println("线程2已将test设置为true"); } catch (InterruptedException e) { e.printStackTrace(); } }); thread1.start(); thread2.start(); } }
为什么你没复现预期的缓存可见性问题?
核心原因是JIT即时编译器的循环优化:当线程1的循环里没有任何实际操作时,JIT会判定这个循环的读取操作是“无意义重复”,直接把!occurrenceCounter.isTest()的读取逻辑优化成只执行一次——也就是从CPU缓存或主存读一次值后,就一直用这个缓存值循环。
换句话说,哪怕线程2已经把test的新值同步到主存,线程1因为被JIT优化,根本不会再去主存重新读取,自然看不到变量更新;偶尔能看到变化,也只是CPU缓存碰巧同步了,属于偶发现象。
正确的复现方案:阻止JIT优化
要复现真实的非volatile变量可见性问题,我们需要在循环里加一些不会被JIT优化掉的简单操作,让JIT无法合并读取操作。比如加一个计数器运算,或者调用Thread.yield():
修改线程1的逻辑:
Thread thread1 = new Thread(() -> { int loopCount = 0; while (!occurrenceCounter.isTest()) { // 简单的计数器运算,阻止JIT优化读取操作 loopCount++; // 或者用Thread.yield(); 也能达到效果 } System.out.println("线程1检测到test变为true,循环执行次数:" + loopCount); });
现在运行代码,你会发现:线程2已经打印“线程2已将test设置为true”,但线程1会一直循环下去(大概率)——因为它一直在读取自己CPU缓存里的旧值,完全看不到主存中test的变化。
背后的原理:volatile的作用
如果给test变量加上volatile修饰符:
private volatile boolean test = false;
JVM会强制保证三个关键语义:
- 可见性:每次读取变量必须从主存读取,每次修改必须立刻同步回主存
- 禁止指令重排序:变量的读写操作不会被JIT或CPU重排序,确保执行顺序和代码编写顺序一致
- 缓存一致性:触发CPU缓存一致性协议(比如MESI),通知其他CPU缓存该变量已失效,需要重新从主存读取
这时候再运行代码,线程1会立刻检测到test的变化,退出循环。
额外注意点
- 可见性问题不是100%必现的,单核CPU机器上几乎无法复现(只有一个缓存),建议在多核CPU环境下测试。
- 如果循环里有
synchronized块、Lock操作,或者调用了System.out.println()(内部带synchronized),这些操作会触发缓存同步,让线程1看到变量变化,无法复现问题。
内容的提问来源于stack exchange,提问作者nayak0765
相关产品推荐
相关产品推荐

