volatile能禁止指令重排是因为其读写操作插入特定内存屏障,确保对该变量的访问有序,但不保证其他变量顺序;它仅解决可见性与部分有序性,无原子性,不能替代synchronized。

volatile 为什么能禁止指令重排
因为 volatile 写操作会在生成的字节码中插入 StoreStore 和 StoreLoad 内存屏障(Memory Barrier),读操作则插入 LoadLoad 和 LoadStore 屏障。这些屏障会强制 JVM 和 CPU 尊重代码顺序,阻止编译器优化和处理器乱序执行对 volatile 变量前后语句的重排。
注意:它只保证对该变量本身的读写具有“有序性”,不保证其他普通变量之间的顺序——比如:a = 1; volatileFlag = true; 中,a = 1 不会被移到 volatileFlag = true 之后;但 volatileFlag = true; b = 2; 中,b = 2 仍可能被提前到写之前(除非 b 也是 volatile)。
volatile 不能替代 synchronized 的场景
volatile 只解决可见性和部分有序性,不提供原子性。常见失效场景包括:
-
i++这类复合操作(读-改-写),即使i是volatile,仍可能丢失更新 - 多个
volatile变量之间存在逻辑依赖,例如先检查ready再读data,但两者都是volatile时,无法保证data已被完全写完(需用volatile+ 正确构造顺序,或锁) - 需要临界区互斥(如防止两个线程同时进入初始化块),
volatile无法阻塞线程
Double-Checked Locking 中 volatile 的关键作用
在单例模式的双重检查锁写法中,volatile 修饰实例字段不是可选的,而是必须的。否则可能因指令重排导致其他线程拿到一个未构造完成的对象引用。
立即学习“Java免费学习笔记(深入)”;
典型错误现象:getInstance() 返回非 null,但调用其方法时抛 NullPointerException 或行为异常。
根本原因在于:JVM 可能将对象分配内存 → 设置引用 → 执行构造函数,重排为:分配内存 → 构造函数 → 设置引用。加 volatile 后,构造函数执行完成前,引用不会被其他线程看到。
示例关键行:
private static volatile Singleton instance;
volatile 的性能开销与适用边界
现代 JVM 对 volatile 读的优化较好(多数情况只是禁止寄存器缓存),写操作因涉及内存屏障和缓存同步,开销略高于普通变量,但远低于 synchronized 或 Lock。
适合场景:
- 状态标志位(如
running,closed) - 一次性安全发布(safe publication),配合正确初始化顺序
- 与
AtomicInteger等配合做简单计数,但要注意复合操作仍需原子类
不适用场景:
- 需要 CAS 或自增/自减原子语义的地方(该用
AtomicInteger) - 多变量协同变更(该用锁或不可变对象)
- 高频写+低频读且对延迟极度敏感的场景(此时要实测对比)
真正容易被忽略的是:volatile 只对直接访问它的线程生效;如果通过反射、Unsafe 或 JNI 绕过 JVM 内存模型,它的约束就不再成立。








