Java 内存模型(JMM):happens-before 原则

前言

现代 CPU、编译器、运行时(如 JVM)为了追求极致的性能,会进行各种优化,比如:

  1. 指令重排序:在不改变单线程程序结果的前提下,优化指令的执行顺序。
  2. CPU 缓存:变量可能被缓存在 CPU 核心各自的缓存中,一个核心的修改不会立刻被另一个核心看到。

这些优化在单线程下没有问题,但在多线程下就会导致严重 Bug(如一个线程写了数据,另一个线程却读到了旧值)。happens-before 规则就是给这些优化设定一个"安全护栏",告诉它们:在这些规则约束下,你们可以自由优化,但不能破坏可见性保证。

happens-before 是 Java 内存模型(JMM)中定义的内存可见性保证: 如果操作 A happens-before 操作 B,那么 A 所做的所有内存写操作对 B 都是可见的。 下面是happens-before常用规则的概览:

规则名称 使用频率 重要性 备注
程序顺序规则 ⭐⭐⭐⭐⭐ 极高 所有代码的基础
锁规则 ⭐⭐⭐⭐⭐ 极高 synchronized/ReentrantLock 的核心
volatile 规则 ⭐⭐⭐⭐⭐ 极高 轻量级同步的标志位
重要的非规则 ⭐⭐⭐⭐⭐ 极高 理解并发问题的关键
线程启动规则 ⭐⭐⭐⭐ 线程间数据传递
线程终止规则 ⭐⭐⭐⭐ 等待线程结果
传递性原则 ⭐⭐⭐ 理论分析组合同步
对象终结规则 finalize() 已弃用,极少使用

1. 程序顺序规则

原则:单个线程内,按照代码顺序执行,前面的操作 happens-before 后面的操作。

伪代码

java 复制代码
// 线程内顺序执行
int x = computeValue();    // 操作 A
int y = process(x);        // 操作 B
// A happens-before B,B 能看到 A 的结果

// JVM 可能重排序,但保证单线程语义不变
a = 1;
b = 2;
c = a + b;                // 总是 3,无论 a=1 和 b=2 是否重排

2. 锁规则

原则:一个锁的解锁操作 happens-before 后续对这个锁的加锁操作。

伪代码

java 复制代码
// 线程1
synchronized(lock) {
    sharedState = new State();   // 写操作
    initialized = true;           // 写操作
}                                // 解锁

// 线程2  
synchronized(lock) {             // 加锁
    // 保证看到线程1的所有修改
    if (initialized) {
        sharedState.use();
    }
}

3. volatile 规则

如果你看到了 volatile 变量的最新值(比如 flag=true),那么你一定能看到在这个 volatile 写之前发生的所有普通写操作

原则:volatile 变量的写操作 happens-before 后续对这个变量的读操作。

伪代码

java 复制代码
// 共享变量
private volatile boolean flag = false;
private int data;

// 线程1(生产者)
data = 42;                // 普通写
flag = true;              // volatile 写 (A)

// 线程2(消费者)
if (flag) {               // volatile 读 (B)
    // A happens-before B
    print(data);          // 保证看到 data=42
}

4. 线程启动规则

原则:Thread.start() 调用 happens-before 线程内的所有操作。

伪代码

java 复制代码
// 主线程
configuration = loadConfig();      // 操作 A
Thread worker = new Thread(() -> {
    // start() happens-before 这里
    process(configuration);        // 操作 B,看到 A 的配置
});
worker.start();                    // A happens-before B

5. 线程终止规则

原则:线程中的所有操作 happens-before 其他线程检测到该线程终止。

伪代码

java 复制代码
// 子线程
Thread worker = new Thread(() -> {
    result = heavyComputation();   // 操作 A
});

worker.start();
worker.join();                     // 等待线程结束

// A happens-before join() 返回
useResult(result);                 // 保证看到计算完成的结果

6. 传递性原则

原则:如果 A happens-before B,且 B happens-before C,则 A happens-before C。

伪代码

java 复制代码
// 线程1
x = 1;                            // 操作 A
synchronized(lock) {              // 进入锁
    y = 2;                        
}                                 // 解锁 (B1)

// 线程2
synchronized(lock) {              // 加锁 (B2)
    // B1 happens-before B2(锁规则)
    print(x);                     // 操作 C
}
// A happens-before C(传递性)

7. 对象终结规则

原则:对象的构造函数结束 happens-before 该对象的 finalize() 方法开始。

伪代码

java 复制代码
public class Resource {
    private final byte[] data;
    
    public Resource(int size) {
        data = new byte[size];     // 构造函数操作 A
        initData();                // 继续初始化
    }
    
    @Override
    protected void finalize() {
        // A happens-before finalize()
        cleanup(data);             // 保证 data 已初始化
    }
}

8. 重要的非规则

原则:没有 happens-before 关系的操作,JVM 可以任意重排序。

伪代码

java 复制代码
// 线程1(无同步)
value = 100;              // 普通写
ready = true;             // 普通写
// 可能被重排为:ready=true, value=100

// 线程2(无同步)
while (!ready) {          // 循环等待 ready 变为 true
    // 忙等待
}
print(value);             // 可能看到 value=0(默认值)!

9. 实际应用原则

同步原则

java 复制代码
// 使用锁建立 happens-before
public synchronized void update() {
    cache.clear();        // 操作 A
    version++;            // 操作 B
}                         // 解锁

public synchronized void read() {
    // 保证看到 update() 中的所有修改
    return cache.get(key);
}

volatile 原则

java 复制代码
private volatile int currentConfigVersion;  // volatile!
private Configuration config;               // 普通变量

public void reloadConfig() {
    Configuration newConfig = loadFromDB();  // 操作 A
    config = newConfig;                      // 普通写 B
    currentConfigVersion++;                  // volatile 写 C
    // B happens-before C (程序顺序规则)
}

public Configuration getConfig() {
    int version = currentConfigVersion;      // volatile 读 D
    // C happens-before D (volatile规则)
    return config;                           // 看到最新的 config
}

线程安全发布原则

java 复制代码
// 安全发布不可变对象
public class ServiceRegistry {
    // 方案1:final 字段
    private final Map<String, Service> services;
    
    public ServiceRegistry() {
        services = loadServices();  // 构造函数内初始化,安全发布
    }
    
    // 方案2:volatile 引用
    private volatile Service[] serviceArray;
    
    public void refresh() {
        Service[] newArray = createNewArray();
        serviceArray = newArray;    // volatile 写,安全发布
    }
}
相关推荐
IMPYLH2 小时前
Lua 的 Package 模块
java·开发语言·笔记·后端·junit·游戏引擎·lua
sunnyday04262 小时前
API安全防护:签名验证与数据加密最佳实践
java·spring boot·后端·安全
Amos_Web2 小时前
Rust实战(五):用户埋点数据分析(前)
后端·架构·rust
梁bk2 小时前
[spring cloud] nacos注册中心和配置中心
后端·spring·spring cloud
oak隔壁找我2 小时前
java-jwt 使用
后端
Hello.Reader2 小时前
连接四元组它为什么重要,以及它和端口复用(SO_REUSEPORT)的关系(Go 实战)
开发语言·后端·golang
想摆烂的不会研究的研究生2 小时前
并发场景——接口幂等性设计
数据库·redis·后端·缓存
sunnyday04262 小时前
Spring AOP 实现日志切面记录功能详解
java·后端·spring
on the way 1232 小时前
day07-Spring循环依赖
后端·spring