volatile能禁止指令重排序,是因为jvm在字节码中插入内存屏障(如storestore、storeload等),对应cpu的mfence等指令,确保写入对其他核可见顺序与程序逻辑一致。

为什么 volatile 能禁止指令重排序?
因为 volatile 不是“加锁”,而是让 JVM 在生成字节码时,自动在读写操作前后插入特定的内存屏障指令——比如对 volatile 变量写入前插 StoreStore,写入后插 StoreLoad;读取时插 LoadLoad 和 LoadStore。这些屏障会翻译成 CPU 级别的 mfence、lfence 或 sfence 指令(取决于平台),真正让硬件按顺序执行。
- 不是所有平台都用
mfence:x86 上StoreLoad用mfence,但 ARM/AArch64 需要更严格的组合,JVM 会自动适配 -
volatile不能替代synchronized:它只保证单变量的可见性和有序性,不保证复合操作原子性(比如counter++) - 别对非
volatile字段“顺带”依赖:即使flag是volatile,也不能假设它前面的普通字段a一定已刷新到主存——除非用StoreLoad屏障把它们捆在一起
线程间看到“a=0 但 flag=true”是怎么发生的?
这是典型的 Store-Store 乱序 + 缓存未同步导致的现象。线程1执行 a = 1 和 flag = true 时,CPU 可能先把 flag 写进 store buffer,再把 a 写进去;而 store buffer 刷到 L1 cache 或主存有延迟。此时线程2从自己的 cache 读到 flag == true,但还没看到 a 的新值——因为 a 还卡在对方 CPU 的 store buffer 里。
- 现象本质不是“指令执行顺序变了”,而是“写入对其他核可见的顺序”和“程序逻辑顺序”不一致
- 仅靠
volatile修饰flag就能解决:它会在flag = true后插入StoreLoad屏障,强制刷出 store buffer 中所有待写数据(包括a = 1) - 如果
a本身也声明为volatile,反而可能多出不必要的屏障开销,不推荐
JIT 编译器重排和 CPU 乱序执行,哪个更该担心?
两者都会发生,但 JIT 重排发生在编译期(生成机器码前),CPU 乱序发生在运行期(执行机器码时);JVM 内存模型规定:只要符合 happens-before 规则,两种重排都允许。实际开发中,你根本分不清 bug 是哪一层引起的——所以统一按 JMM 规则编码即可。
- 不要试图“看汇编”验证重排:JIT 可能在不同运行阶段生成不同代码,且不同 CPU 架构表现不同
-
final字段的初始化安全,靠的是 JMM 对构造器末尾隐式插入StoreStore屏障,不是靠 CPU 不乱序 - 使用
Unsafe.putOrderedXXX时要注意:它只提供 StoreStore / LoadStore 语义,不提供 StoreLoad,适合计数器等场景,但不能用于发布对象
写双重检查单例时,为什么必须用 volatile?
因为对象构造的四步(分配内存 → 初始化 → 引用赋值)中,JIT 和 CPU 都可能把“引用赋值”(astore_1)提前到“初始化完成”之前。没有 volatile,线程2可能拿到一个 instance != null 但内部字段仍是默认值的对象。
立即学习“Java免费学习笔记(深入)”;
- 关键点不在“构造是否完成”,而在“引用写入是否对其他线程立即可见”以及“是否强制初始化写入已刷出”
- 只加
synchronized不够:虽然能保证原子性,但若没volatile,线程2仍可能读到未完全初始化的instance(因缺少 StoreLoad 屏障) - 别用
AtomicReference替代volatile:它底层也是靠 volatile 语义,但额外开销大,无必要
真正难调试的从来不是“能不能重排”,而是“什么时候恰好重排+缓存没同步+线程调度又卡在临界点”。与其猜硬件行为,不如老老实实按 JMM 写:该 volatile 就 volatile,该锁就锁,别省那几纳秒。










