JMM通过抽象规则、内存屏障和关键字语义约束编译器/JVM/CPU行为,提供可见性、有序性、原子性的可预期工具而非天然保障;volatile解决单变量读写的可见性与有序性(靠主存读写+内存屏障),但不保证复合操作原子性;synchronized通过锁机制同步工作内存、强制串行执行和指令屏障,同时保障三大特性;final字段借助构造器内隐式屏障实现安全发布下的初始化可见性,但依赖正确构造与无逸出。

JMM 不直接“保障”有序性、原子性、可见性,而是通过一套抽象规则 + 内存屏障 + 关键字语义,约束 JVM、编译器和 CPU 的行为,让开发者能写出可预期的并发代码。 它不保证“天然正确”,只提供可被正确使用的工具和边界。踩错边界,照样出问题。
volatile 怎么解决可见性和有序性但不解决原子性
声明 volatile int flag = 0 后,每次读 flag 都强制从主内存加载,每次写都立即刷回主内存——这破除了工作内存缓存导致的“看不到更新”的可见性问题。
同时,JMM 要求在 volatile 读/写前后插入内存屏障:
- 写操作后加 StoreStore 屏障,禁止其后的普通写重排序到它前面;
- 读操作前加 LoadLoad 屏障,禁止其前的普通读重排序到它后面。
这就锁住了部分重排序,提供了“单变量读写”的有序性保障。
但它不解决原子性,比如 flag++ 是“读-改-写”三步,在 volatile 下仍可能被其他线程打断。常见错误是以为加了 volatile 就能安全做自增、计数器或状态切换(如 running = false 后立刻 break),结果发现逻辑没按预期退出——因为判断和退出之间没有原子保护。
- 适用场景:纯状态标志(on/off)、一次性发布(如单例对象初始化完成)
- 不适用场景:复合操作(
count++、list.add(x))、依赖检查再执行的逻辑(if flag then doSomething) - 注意:volatile 对 long/double 的 64 位写在旧 JVM 上可能非原子,虽现代 JVM 已默认保证,但不要依赖
synchronized 如何同时覆盖三大特性
synchronized 块进入时触发 monitorenter 指令,会清空当前线程工作内存中所有共享变量的副本;退出时触发 monitorexit,强制将修改过的变量全部刷新回主内存。这个“清空+刷新”机制天然解决了可见性。
它的互斥本质(同一时刻最多一个线程持有 monitor)把临界区代码变成串行执行,从而把原本非原子的操作(如 i++)包裹成不可分割的整体——原子性由此而来。
有序性则靠 JVM 对 monitor 指令的语义约束:monitorenter 和 monitorexit 本身具有内存屏障效果,禁止指令重排序跨越锁边界。例如,临界区内的读写不会被提到锁外,也不会被锁外的读写穿插进来。
- 性能代价明显:涉及操作系统 mutex、上下文切换开销,且无法中断等待
- 注意:synchronized 锁的是对象监视器(monitor),不是变量本身;锁不同对象等于没锁
- 常见误用:用
this锁方法,但调用方持的是不同实例,实际未形成竞争
为什么 final 字段能保证初始化完成后的可见性
final 字段在构造器内完成赋值后,JMM 规定:只要对象构造完成(即构造器返回),其他线程看到该对象引用时,必定能看到 final 字段的正确值——哪怕没用 synchronized 或 volatile。
原理是:JVM 在 final 字段写入后、构造器结束前,隐式插入 StoreStore 屏障,防止该写入被重排序到构造器末尾之后;同时,其他线程首次读取该对象引用时,也会插入 LoadLoad 屏障,确保能看到所有 final 字段的写入结果。
但这仅适用于“正确构造”的对象:final 字段不能在构造过程中逸出(如 this 引用在构造器结束前被发布出去),否则其他线程可能看到未初始化完毕的 final 字段。
- 典型用途:不可变对象(Immutable)、安全发布(safe publication)
- 陷阱:final 只保字段值不变,不保字段指向对象的内部状态;若 final List 被外部修改,仍需额外同步
- 注意:static final 常量(编译期常量)由类加载器保证,与 JMM 的 final 规则无关
真正容易被忽略的点是:JMM 的所有保障都建立在“正确使用”前提下。比如 volatile 的有序性只对它自己读写生效,不辐射周边普通变量;synchronized 的原子性只限于括号内代码;final 的安全发布必须配合无逸出构造。一旦跳出这些边界,JMM 不背锅——它只是说明书,不是保险柜。

















