volatile写前插入storestore屏障,确保其前所有普通写操作先完成并刷新到主内存;写后插入storeload屏障,防止与后续读操作重排序;读后插入loadload和loadstore屏障,禁止后续读/写重排到该读之前。

volatile 写操作前插入 StoreStore 屏障
当对一个 volatile 变量执行写操作(如 flag = true)时,JVM 会在该写指令之前插入 StoreStore 屏障。它的作用是:禁止上面的普通写操作被重排序到该 volatile 写之后。
常见错误现象:在 DCL 单例中,如果不用 volatile 修饰 instance,可能出现「对象已分配内存地址、但构造函数尚未执行完」就被其他线程读取到的情况——这就是因为 new Singleton() 的三步(分配内存、调用构造器、赋值给引用)被重排了。
实操建议:
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
- 把
volatile加在单例引用上,确保「构造完成」这个语义对其他线程可见 - 不要依赖
synchronized块外的普通变量赋值顺序来推断逻辑先后 -
StoreStore不影响读操作,所以它只约束写-写顺序,不解决读写乱序问题
volatile 读操作后插入 LoadLoad 屏障
对 volatile 变量执行读操作(如 if (flag))时,JVM 会在该读指令之后插入 LoadLoad 屏障。它保证:该读操作之后的所有普通读操作,不会被重排序到该 volatile 读之前。
使用场景:配合状态标志做条件检查。例如先读 volatile 标志位,再读后续依赖该状态的普通字段。没有 LoadLoad,JIT 可能把后面字段的读提前,导致读到未初始化的值。
实操建议:
- 标志变量必须是
volatile,否则无法触发LoadLoad - 标志之后读的普通变量,不能是局部变量或栈上临时值(它们本就不涉及主内存同步)
- 这个屏障不强制刷新缓存,只起排序约束;可见性靠的是前面的
volatile写 +LoadLoad组合生效
volatile 读写组合形成完整的 happens-before 链
volatile 的禁止重排能力,本质是通过读写两侧的内存屏障共同构成一个跨线程的顺序约束。JMM 规定:volatile 写先行发生于(happens-before)后续任意线程对该 volatile 变量的读。
性能影响:相比 synchronized,volatile 不加锁、无上下文切换开销,但每次读写都带屏障指令,在高竞争场景下可能比普通变量慢 2–5 倍(取决于 CPU 架构)。
容易踩的坑:
- 误以为
volatile能保证i++原子性——它只管读写顺序和可见性,不锁住操作过程 - 在非 x86 平台(如 ARM)上,
volatile读写会生成更重的屏障指令(如dmb ish),行为更严格,但代价更高 - 如果读写之间有大量无关计算,JIT 可能仍会优化掉部分中间指令,屏障只保序,不保“中间不被干扰”
如何验证 volatile 确实插入了屏障
直接看字节码看不出内存屏障,但可以通过 -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly 查看 JIT 编译后的汇编。x86 上,volatile 写后通常紧跟着 lock addl $0x0,(%rsp),读前常有 mov 后立即跟 lock addl 或 mfence 类指令。
更实用的办法是用 JOL(Java Object Layout)+ JCStress 工具做压力测试:构造两个线程分别执行写和读,观察是否出现「写完但读不到」或「读到半初始化对象」的现象。一旦去掉 volatile,失败率会显著上升。
关键点在于:内存屏障不是“让代码变慢”的副作用,而是 JVM 对硬件缓存协议(如 MESI)的主动适配——你写的每一条 volatile 语句,都在悄悄调用 CPU 的缓存一致性机制。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










