Java内存模型JMM与HappensBefore

Java内存模型JMM与HappensBefore

Java 内存模型(JMM)定义了多线程环境下共享变量的访问规则,是理解并发编程的理论基础。

JMM 抽象结构

主内存与工作内存

     主内存(Shared Memory)
┌─────────────────┐
│ x = 0 │
│ y = 0 │
└─────────────────┘
▲ │
read │ │ write
load │ │ store
│ ▼
┌───────────┐ ┌───────────┐
│ 线程A │ │ 线程B │
│ 工作内存 │ │ 工作内存 │
│ x = ? │ │ x = ? │
└───────────┘ └───────────┘
  • 主内存:所有共享变量的存储区域
  • 工作内存:每个线程的私有拷贝

内存交互操作

操作 作用
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)
int b = 2; // (2)

// (1) happens-before (2)
// 但JMM允许重排序,只要不改变单线程执行结果

2. 监视器锁规则

解锁 happens-before 后面对同一锁的加锁。

synchronized (lock) {
x = 1; // 解锁前
}
// happens-before
synchronized (lock) {
int y = x; // 加锁后,保证看到x=1
}

3. volatile 规则

volatile 写 happens-before 后面对同一变量的 volatile 读。

volatile int x;

// 线程A
x = 1; // volatile写

// 线程B
int y = x; // volatile读,保证看到x=1

4. 线程启动规则

Thread.start() happens-before 线程内的所有操作。

int x = 1;
Thread t = new Thread(() -> {
int y = x; // 保证看到x=1
});
t.start();

5. 线程终止规则

线程内的所有操作 happens-before 检测到线程终止。

Thread t = new Thread(() -> {
x = 1; // 线程内操作
});
t.start();
t.join(); // 等待线程结束
int y = x; // 保证看到x=1

6. 中断规则

interrupt() happens-before 检测到中断。

thread.interrupt();

// 线程内
if (Thread.interrupted()) {
// 能检测到中断
}

7. 对象终结规则

构造函数执行 happens-before finalize()

public class Resource {
private final int value;

public Resource(int v) {
this.value = v; // happens-before finalize()
}

@Override
protected void finalize() {
System.out.println(value); // 保证看到正确值
}
}

8. 传递性

如果 A happens-before B,B happens-before C,那么 A happens-before C。

volatile int x;
int y;

// 线程A
y = 1; // (1)
x = 2; // (2) volatile写

// 线程B
int a = x; // (3) volatile读,看到x=2
int b = y; // (4) 由于(1)hb(2), (2)hb(3), 所以(1)hb(4),看到y=1

as-if-serial 语义

单线程程序的执行结果必须与按程序顺序执行的结果一致,编译器和处理器可以进行不影响单线程结果的优化。

int a = 1;      // (1)
int b = 2; // (2)
int c = a + b; // (3)

// (1)和(2)可以重排序,因为(3)依赖它们
// 但(3)不能重排序到(1)或(2)之前

重排序的类型

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写
StoreStore屏障 // 前面的普通写不能重排序到后面
write volatile
StoreLoad屏障 // 防止volatile写与后面的读重排序

// volatile读
LoadLoad屏障 // 防止后面的普通读重排序到前面
LoadStore屏障 // 防止后面的普通写重排序到前面
read volatile

双重检查锁定的正确实现

public class Singleton {
private static volatile Singleton instance;

public static Singleton getInstance() {
if (instance == null) { // (1)
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // (2) volatile写
}
}
}
return instance; // (3) volatile读
}
}

为什么需要 volatile?

new Singleton() 分三步:

  1. 分配内存
  2. 初始化对象
  3. 赋值给引用

步骤 2 和 3 可能重排序。没有 volatile:

  • 线程 A 执行到 3 但未完成 2
  • 线程 B 在 (1) 看到非 null,返回未初始化的对象

volatile 禁止了这种重排序,保证线程安全。

总结

JMM 通过 Happens-Before 规则定义了内存可见性的保证:

  1. 无需同步:单线程内的 happens-before 关系自然成立
  2. synchronized/volatile:显式建立 happens-before
  3. 线程启动/终止:Thread 类的方法隐式建立 happens-before

理解 JMM 和 happens-before,是写出正确并发代码的理论基础。


   转载规则


《Java内存模型JMM与HappensBefore》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录