volatile读插入loadload+loadstore屏障,写插入storestore+storeload屏障;它仅保障单次读写可见性与有序性,不保证复合操作原子性。

Java里的volatile到底插了哪道内存屏障?
它不是“加个关键字就线程安全”,而是编译器和CPU执行时被强制插入特定屏障指令。JVM对volatile字段的读写,会在字节码层面生成getfield/putfield,再由JIT在生成机器码时按JSR-133规范插入屏障——读操作前加LoadLoad+LoadStore,写操作后加StoreStore+LoadStore。
常见错误是以为volatile能保证复合操作原子性,比如i++(读-改-写三步),其实只对单次读或写生效。它不阻止重排序,只是让重排序遵守“禁止越过屏障”的边界。
-
volatile写:禁止该写与前面任何读/写重排序(StoreStore);也禁止该写与后面任意读重排序(StoreLoad) -
volatile读:禁止该读与后面任何读/写重排序(LoadLoad + LoadStore) - 注意:x86平台没有真正的StoreLoad屏障指令(靠
mfence或lock addl模拟),但JVM仍会生成,因为语义必须跨架构一致
JMM如何把Java代码映射成CPU级屏障指令?
Java内存模型(JMM)是抽象契约,不规定具体指令,但HotSpot在不同CPU上会选最轻量等效实现。比如在x86上,volatile读几乎不需额外指令(靠缓存一致性协议MESI保障可见性),而volatile写需lock addl $0, (rsp)这类带锁前缀的空操作来触发StoreStore+StoreLoad语义。
ARM或RISC-V就没这么幸运:它们弱内存模型,每次volatile读都要ldar(load-acquire),写都要stlr(store-release),否则可能看到乱序结果。
立即学习“Java免费学习笔记(深入)”;
- x86下
volatile写开销极小,常被误认为“没代价”,但这是架构红利,不是语言特性 - ARM64上
volatile读写都带明确acquire/release语义,性能影响比x86明显 - 不要依赖
Unsafe.loadFence()/Unsafe.storeFence()去手动补屏障——它们是内部API,JDK 9+已标记为@Deprecated(forRemoval = true)
为什么synchronized块里不用volatile也能保证可见性?
因为synchronized退出时隐式插入了StoreStore+StoreLoad屏障(对应monitorexit字节码),进入时插入LoadLoad+LoadStore(对应monitorenter)。这和volatile写+读的组合效果一致,但作用范围是整个临界区。
典型误区是给synchronized里的变量再加volatile修饰——既无必要,还可能误导后续维护者以为该字段需独立同步。
- 临界区外的变量修改,若未通过同步机制发布,即使在块内赋值,其他线程仍可能看不到最新值
-
synchronized提供的是“原子性+可见性+有序性”三重保障,volatile只保后两者 - 如果只是想防止指令重排(如双重检查锁里的instance赋值),
volatile比synchronized更轻量,但别指望它锁住逻辑
使用VarHandle替代volatile时屏障行为有啥变化?
JDK 9引入的VarHandle用getVolatile/setVolatile方法提供更细粒度控制,底层仍走相同屏障逻辑,但绕过了字段访问检查,且支持数组元素、堆外内存等场景。
容易踩的坑是直接用get/set(非volatile版本)——它们不带任何屏障,等价于普通变量访问,哪怕目标字段本身是volatile修饰的也没用。
-
VarHandle.getVolatile()≡ 字段上volatile读,但可动态绑定到任意内存位置 -
VarHandle.compareAndSet()在x86上编译为cmpxchg,自带完整屏障;在ARM上则是cas指令,同样满足acquire-release语义 - 别用
VarHandle.getOpaque()做并发通信——它只禁止单向重排序(如禁止该读与前面读重排),不保证可见性
真正难的是理解“屏障不是魔法,它只约束指令顺序,不解决数据竞争”。比如两个线程同时对同一volatile int执行incrementAndGet(),结果仍可能错——因为CAS循环里包含非原子的读+计算+写三步,屏障只管每一步的边界,不管中间逻辑是否串行。










