JMM及其相关
Java 内存模型(Java Memory Model,简称 JMM)是 Java 并发编程的基础。它不是真实存在的物理内存布局,而是一套由 Java 虚拟机(JVM)定义的规范和协议。
JMM 决定了一个线程对共享变量的写入何时对另一个线程可见,旨在屏蔽硬件和操作系统的内存访问差异,实现 Java 程序在各种平台下一致的并发行为。
1. 为什么需要 JMM?
在现代计算机体系中,为了弥补 CPU 运算速度与内存 IO 速度的巨大鸿沟,硬件设计引入了多级缓存(L1, L2, L3)。同时,为了优化执行效率,编译器和处理器会对指令进行重排序。
这些优化在多线程环境下带来了两个严峻挑战:
- 可见性问题:线程 A 在 CPU 缓存中修改了变量,但尚未同步到主内存,线程 B 看到的仍是旧值。
- 有序性问题:指令重排(Reordering)可能导致代码执行顺序被打乱。虽然单线程下有 as-if-serial 语义保证结果不变,但在多线程下会产生逻辑错误(如 DCL 单例中对象尚未初始化完毕就被引用)。
2. JMM 的抽象结构
JMM 规定所有的共享变量都存储在 主内存 (Main Memory) 中,每个线程都有自己的 工作内存 (Working Memory)。
- 主内存:物理上对应计算机的 RAM。
- 工作内存:线程私有的空间。物理上对应 CPU 寄存器和缓存。线程必须先将变量拷贝到工作内存才能操作。
3. 并发编程的三大特性
3.1 原子性
指操作不可分割。synchronized 和原子类等可保证原子性。
3.2 可见性
指一线程修改后,其他线程立即可见。主要靠 volatile 触发缓存同步保证。
3.3 有序性
指程序执行的顺序。volatile 通过内存屏障禁止指令重排来保证。
4. volatile 深度原理:内存屏障
volatile 解决可见性和有序性的底层机制是完全相同的,都依赖于 内存屏障。这些屏障规定了指令执行的边界。
4.1 如何保证“有序性” (禁止重排)
Volatile 可以防止指令重排序。啥叫重排序?就是编译器和 CPU 为了效率高,可能会把代码执行顺序调整一下。但多线程下,这可能会出问题。
JVM 在 volatile 读写操作时插入特定的屏障,阻断了指令跨越屏障进行重排序的可能性:
- 写操作前的屏障:禁止上面的普通写和下面的
volatile写重排序。这保证了在volatile变量生效前,之前的所有写入都已完成。 - 写操作后的屏障:防止上面的
volatile写与下面可能的volatile读/写重排序。这是全能型屏障,确保写操作彻底同步。 - 读操作后的屏障:防止上面的
volatile读与下面后续的读/写操作重排序。这保证了读取到最新值后,后续逻辑才开始执行。
4.2 如何保证“可见性” (数据同步)
- 写 Volatile 变量时:JVM 在写完后加个写屏障,把变量的新值刷到主内存,确保其他线程能看到。比如线程 A 写
volatile int x = 10;,写屏障大声喊:“我写完了!x 现在是 10!”保证其他线程立马知道。 - 读 Volatile 变量时:JVM 在读之前加个读屏障,强制问一句:“你们写完了吗?我要读最新的!”然后从主内存拿最新值。比如线程 B 读
x,读屏障确保拿到的是 10,而不是缓存里的 0。
4.3 典型案例:DCL 单例模式
如果不加 volatile,instance = new Singleton() 可能被错误地拆解为:1.分配空间 -> 2.赋值给 instance -> 3.初始化对象(正确为132)。 也就是另一个线程可能拿到一个尚未初始化完成的非空对象。volatile 的内存屏障严禁 2 和 3 重排,从而保证了安全性。
5. Happens-Before 原则
JMM 提供的更高层抽象规则,只要符合以下规则,JMM 保证操作 A 的结果对 B 可见:
- 程序次序规则:单线程内代码顺序执行。
- 锁定规则:unlock 必然发生在后续对同锁的 lock 之前。
- volatile 变量规则:对 volatile 的写操作先行发生于后面对它的读操作。
- 传递性:A -> B, B -> C 则 A -> C。