Java内存模型JMM与HappensBefore
Java 内存模型(JMM)定义了多线程环境下共享变量的访问规则,是理解并发编程的理论基础。
JMM 抽象结构
主内存与工作内存
主内存(Shared Memory) |
- 主内存:所有共享变量的存储区域
- 工作内存:每个线程的私有拷贝
内存交互操作
| 操作 | 作用 |
|---|---|
| lock | 锁定主内存变量 |
| unlock | 解锁主内存变量 |
| read | 从主内存读取到工作内存 |
| load | 将 read 的值放入工作内存变量 |
| use | 使用工作内存变量值 |
| assign | 给工作内存变量赋值 |
| store | 将工作内存变量值传送到主内存 |
| write | 将 store 的值写入主内存变量 |
Happens-Before 规则
Happens-Before 是 JMM 中保证可见性的核心概念:如果 A happens-before B,那么 A 的结果对 B 可见。
1. 程序次序规则
一个线程内,前面的操作 happens-before 后面的操作。
int a = 1; // (1) |
2. 监视器锁规则
解锁 happens-before 后面对同一锁的加锁。
synchronized (lock) { |
3. volatile 规则
volatile 写 happens-before 后面对同一变量的 volatile 读。
volatile int x; |
4. 线程启动规则
Thread.start() happens-before 线程内的所有操作。
int x = 1; |
5. 线程终止规则
线程内的所有操作 happens-before 检测到线程终止。
Thread t = new Thread(() -> { |
6. 中断规则
interrupt() happens-before 检测到中断。
thread.interrupt(); |
7. 对象终结规则
构造函数执行 happens-before finalize()。
public class Resource { |
8. 传递性
如果 A happens-before B,B happens-before C,那么 A happens-before C。
volatile int x; |
as-if-serial 语义
单线程程序的执行结果必须与按程序顺序执行的结果一致,编译器和处理器可以进行不影响单线程结果的优化。
int a = 1; // (1) |
重排序的类型
1. 编译器重排序
编译器在不改变单线程语义的前提下调整指令顺序。
2. 指令级并行重排序
处理器利用指令级并行技术改变指令执行顺序。
3. 内存系统重排序
由于缓存和缓冲,存储操作看起来是按不同的顺序完成的。
内存屏障
JMM 通过内存屏障限制重排序:
| 屏障类型 | 示例 | 作用 |
|---|---|---|
| LoadLoad | Load1; LoadLoad; Load2 | 禁止Load1和Load2重排序 |
| StoreStore | Store1; StoreStore; Store2 | Store1必须在Store2之前 |
| LoadStore | Load1; LoadStore; Store2 | Load1必须在Store2之前 |
| StoreLoad | Store1; StoreLoad; Load2 | Store1必须在Load2之前,全能屏障 |
volatile 的内存屏障
// volatile写 |
双重检查锁定的正确实现
public class Singleton { |
为什么需要 volatile?
new Singleton() 分三步:
- 分配内存
- 初始化对象
- 赋值给引用
步骤 2 和 3 可能重排序。没有 volatile:
- 线程 A 执行到 3 但未完成 2
- 线程 B 在 (1) 看到非 null,返回未初始化的对象
volatile 禁止了这种重排序,保证线程安全。
总结
JMM 通过 Happens-Before 规则定义了内存可见性的保证:
- 无需同步:单线程内的 happens-before 关系自然成立
- synchronized/volatile:显式建立 happens-before
- 线程启动/终止:Thread 类的方法隐式建立 happens-before
理解 JMM 和 happens-before,是写出正确并发代码的理论基础。