volatile 解决多线程下变量修改不被立即可见的问题,通过强制主内存读写和插入内存屏障禁止重排序,但不保证复合操作原子性。

volatile 关键字到底解决了什么问题
Java 多线程中变量修改不被其他线程立即看到,根本原因不是“线程没刷新”,而是 JMM 允许每个线程拥有自己的工作内存(缓存),对 sharedVar 的读写可能只发生在本地副本上。用 volatile 修饰后,每次读都强制从主内存加载,每次写都立即刷回主内存,并插入内存屏障禁止重排序。
常见错误现象:while (!flag) { Thread.sleep(100); } 中 flag 被另一个线程设为 true,但当前线程永远不退出——因为 flag 没加 volatile,JIT 可能将其优化为常量判断或缓存到寄存器。
-
volatile不能替代锁:它不保证复合操作的原子性(如i++) - 适用于“一写多读”且无依赖关系的场景,比如状态标志、单次发布的对象引用
- 在 JDK 1.5+ 后语义已严格基于 JMM 定义,旧版(如 1.4)行为不可靠
synchronized 和 final 怎么参与可见性保障
synchronized 块的进入和退出分别具有“读屏障”和“写屏障”效果:进入时清空本地工作内存,强制从主内存重新读取共享变量;退出时将本线程所有修改刷回主内存。这比 volatile 更重,但能同时解决原子性和可见性。
final 字段的可见性是编译期+运行期协同保证的:构造器内对 final 字段的写入,在构造完成前就对其他线程可见(前提是该对象未在构造过程中逸出)。这是通过禁止编译器/JIT 将 final 写操作重排序到构造器末尾之后实现的。
- 不要在构造器里把
this引用发布出去(如注册监听器、启动线程),否则final字段可能被看到默认值 -
synchronized(this)和synchronized(staticObj)的可见性作用域不同:前者只同步实例变量,后者才影响静态变量 - JDK 9+ 中
VarHandle的setRelease/getAcquire提供了更细粒度的控制,语义接近volatile但可指定内存序
为什么 AtomicXXX 类能保证可见性却不显式用 volatile
以 AtomicInteger 为例,其内部 value 字段声明为 volatile:
private volatile int value;所有原子方法(如
incrementAndGet())底层调用的是 Unsafe.compareAndSwapInt(),该指令本身具有内存屏障语义,且会触发 JVM 对 volatile 字段的读写协议。
容易忽略的点:AtomicInteger.get() 等读方法之所以可见,不是因为 CAS,而是因为字段本身是 volatile;而 lazySet()(即 setRelease)则只保证写入不重排序到之后,但不强制立即刷回主内存,适合性能敏感的“单向通知”场景。
- 不要误以为
AtomicXXX的所有方法都等价于加锁——weakCompareAndSet在某些平台可能失败而不抛异常,也不提供强顺序保证 - 若需对多个变量做原子更新,
AtomicReferenceFieldUpdater或VarHandle比锁更轻量,但要求字段必须是volatile
JMM 源码里哪几行真正定义了 happens-before 规则
JMM 并没有一行“源码”直接定义 happens-before,它是规范层面的抽象模型。但在 OpenJDK 实现中,关键逻辑体现在:hotspot/src/share/vm/opto/graphKit.cpp 中的内存屏障插入策略、hotspot/src/cpu/x86/vm/macroAssembler_x86.cpp 中对 membar 指令的生成、以及 hotspot/src/share/vm/runtime/synchronizer.cpp 中 monitor enter/exit 的内存语义处理。
真正影响开发者的是 Java 语言规范(JLS)第 17.4 节规定的 7 条 happens-before 规则,它们被编译器和 JVM 实现为具体屏障指令。比如“监视器锁规则”对应 synchronized 块退出时插入 StoreLoad 屏障,“volatile 变量规则”对应每次 volatile 写后插入 StoreStore + StoreLoad。
- happens-before 是偏序关系,不是时间先后——A happens-before B,不代表 A 一定在 B 之前执行,只代表 A 的结果对 B 可见
- JIT 编译器在满足 happens-before 前提下,仍可自由重排序指令,这是性能优化的基础,也是理解“看似乱序却不出错”的关键
- 调试时用
-XX:+PrintAssembly查看汇编,能看到lock addl $0x0,(%rsp)这类隐式内存屏障,就是 JVM 插入的
volatile 就万事大吉,却忘了它不阻止指令重排对非 volatile 字段的影响。Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










