volatile写操作触发storestore+storeload屏障,确保前面所有普通写不重排到其后、其后的读不提前到之前;volatile读对应loadload+loadstore屏障,禁止后续读写重排到其前。

volatile写操作触发的是StoreStore + StoreLoad屏障
Java里volatile变量的写,底层不是简单发一条“内存屏障指令”,而是由JVM根据目标CPU架构(x86/ARM等)插入特定组合的屏障——在x86上,volatile写实际只生成一个mov加lock xchg(隐含Full Barrier),但语义上它必须保证StoreStore和StoreLoad两重效果:前面所有普通写不能重排到它之后,它之后的读也不能提前到它之前。
- 常见错误现象:
volatile写后紧跟一个非volatile读,却误以为能保证该读看到之前所有写——其实不行,得靠StoreLoad屏障兜底,而x86恰好默认提供,ARM则必须显式插入 - 使用场景:双检锁单例中
instance = new Singleton()这行必须写入volatile字段,否则构造函数内联后可能被重排成“分配内存→写volatile字段→初始化对象”,导致其他线程拿到未初始化的对象 - 参数差异:没有“可配置的屏障类型”;
volatile语义固定,JVM自动映射为对应硬件指令,开发者无法干预插入位置或强度
volatile读操作对应LoadLoad + LoadStore屏障
volatile读不只是“读得新”,关键是它禁止编译器和CPU把后面的读/写重排到它前面。比如if (flag) { return data; }中flag是volatile,那data的读就不会被提到flag判断之前——这是LoadLoad+LoadStore共同作用的结果。
- 常见错误现象:把
volatile读当成“强制刷新缓存”,在非x86平台(如旧版ARMv7)上,仅靠volatile读无法保证看到最新值,还需配合acquire语义(Java 9+的VarHandle.acquireRead才明确表达这点) - 性能影响:x86上
volatile读几乎零开销(只是普通mov),但ARM需插入dmb ish,有微小延迟;高频读volatile布尔标志位在ARM上比x86更敏感 - 兼容性注意:Java 5之前
volatile读写不保证happens-before,老代码迁移时若依赖旧JVM行为,可能暴露重排序问题
内存屏障不是Java关键字,也不能手动调用
Java没有mfence、lfence这类直接暴露给程序员的屏障指令。所谓“volatile底层用内存屏障”,是JVM在生成汇编时做的事,你写的Java代码里永远看不到StoreLoadBarrier()这种调用。
- 容易踩的坑:有人试图用
Unsafe.storeFence()或Unsafe.fullFence()替代volatile,但这些API不稳定、无文档保障,且在不同JDK版本中行为可能变化(如JDK 17已强烈限制Unsafe使用) - 使用场景:只有极少数框架(如Disruptor、Aeron)在绕过JVM抽象层做极致优化时才会碰
Unsafe屏障,业务代码一律用volatile或java.util.concurrent工具类 - 替代方案:需要更强语义时,优先选
VarHandle(Java 9+)的getAcquire/setRelease,它们比volatile更精确,又比Unsafe安全
重排序禁令只发生在volatile变量本身的操作之间
很多人以为只要用了volatile,整个方法里的所有读写就都不重排了。错。JVM只保证对该volatile变量的读写操作,与其他内存访问之间满足JSR-133规定的happens-before约束,其余普通变量照常可能被重排。
立即学习“Java免费学习笔记(深入)”;
- 典型反例:
int a = 1; volatile boolean flag = true; int b = 2;中,a=1和b=2仍可能被重排到flag=true两侧,只要不破坏对flag本身的可见性规则 - 为什么这样做:过度禁止重排序会严重拖慢性能,JVM只在必要处设防——也就是volatile变量的读写点,这是平衡正确性与性能的关键设计
- 调试难点:这种局部性让问题难以复现,尤其在多核ARM设备上,可能某次测试全通过,换台机器就偶发空指针——根源往往是没意识到重排序只锚定在volatile变量上
真正难的不是记住“volatile加了什么屏障”,而是理解它只在volatile变量的读写点上建起一道窄门,门内秩序井然,门外一切照旧。多数并发bug,都出在这道门没被正确立在该立的地方。










