06-Java内存模型(JMM)与Happens-Before规则详解

14 阅读4分钟

Java内存模型(JMM)与Happens-Before规则详解

一、为什么需要内存模型?

1. 硬件层面的挑战

硬件特性引发的问题JMM的解决方案
CPU缓存一致性多核缓存数据不一致定义主内存与工作内存交互规则
指令重排序程序执行顺序与代码顺序不一致Happens-Before规则约束
多级存储架构内存访问速度差异显著内存屏障指令控制可见性

2. 并发编程三要素

graph TD
    A[原子性] -->|synchronized| B(操作不可中断)
    C[可见性] -->|volatile| D(修改立即可见)
    E[有序性] -->|Happens-Before| F(禁止特定重排序)

二、JMM核心结构解析

1. 内存交互八大操作

操作类型作用等效代码示例
lock锁定主内存变量synchronized(obj) {
read从主内存读取到工作内存int localVar = sharedVar;
load将read值放入工作内存副本
use线程使用变量值if (localVar > 0) {...}
assign给工作内存变量赋值localVar = 42;
store将工作内存值传回主内存
write将store值写入主内存变量sharedVar = localVar;
unlock释放主内存锁}

2. 内存屏障类型(以x86为例)

// StoreStore屏障示例
public class MemoryBarrier {
    int x;
    volatile boolean v;
    
    void writer() {
        x = 42;          // 普通写
        StoreStore;       // 隐式屏障(由volatile写入触发)
        v = true;         // volatile写
    }
    
    void reader() {
        if (v) {          // volatile读
            LoadLoad;      // 隐式屏障
            System.out.println(x); // 保证看到x=42
        }
    }
}

三、Happens-Before规则全解

1. 规则列表与案例

规则名称具体描述代码示例
程序顺序规则同一线程内操作按代码顺序执行int a=1; int b=a; // b一定能看到a=1
volatile变量规则volatile写先于后续任意线程的读volatile int x; x=1; → 其他线程读x必见1
线程启动规则Thread.start()前操作对线程内可见int var=1; new Thread(()->System.out.println(var)).start(); // 必输出1
线程终止规则线程所有操作先于其他线程检测到其终止t.join(); → 主线程可见t线程的所有修改
中断规则线程interrupt()调用先于被中断线程检测中断thread.interrupt();threadisInterrupted()必为true

2. 传递性证明

class HBTransitivity {
    int x = 0;
    volatile boolean v = false;
    
    void write() {
        x = 42;      // (1) 普通写
        v = true;    // (2) volatile写
    }
    
    void read() {
        if (v) {     // (3) volatile读
            System.out.println(x); // 保证输出42
            // (1) happens-before (2) 
            // (2) happens-before (3)
            // 根据传递性 → (1) happens-before (3)
        }
    }
}

四、实战:DCL单例与volatile

1. 错误实现分析

class Singleton {
    private static Singleton instance;
    
    static Singleton getInstance() {
        if (instance == null) {               // 第一次检查(未同步)
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 可能发生指令重排序!
                }
            }
        }
        return instance;
    }
}

问题根源new Singleton()可能被重排序为:

  1. 分配内存空间
  2. 将引用指向内存(instance非null)
  3. 初始化对象(此时其他线程可能访问到未初始化的对象)

2. 正确解决方案

class SafeSingleton {
    private static volatile SafeSingleton instance;
    
    static SafeSingleton getInstance() {
        if (instance == null) {
            synchronized (SafeSingleton.class) {
                if (instance == null) {
                    instance = new SafeSingleton(); // volatile禁止重排序
                }
            }
        }
        return instance;
    }
}

volatile作用

  • 禁止步骤2与步骤3的重排序
  • 保证修改对所有线程立即可见

五、常见问题QA

💬 Q1:final字段是否受JMM约束?

答案: final字段有特殊的内存语义:

  • 正确构造的对象中,final字段初始化值对所有线程可见(无需同步)
  • 但若对象引用逃逸(如构造函数中发布this引用),可能看到未初始化的final字段

💬 Q2:synchronized是否保证有序性?

答案: 是的!synchronized的解锁操作包含:

  1. 将工作内存刷新到主内存(Store)
  2. 插入StoreStore屏障(禁止写重排序)
  3. 插入StoreLoad屏障(保证后续读能看到最新值)

💬 Q3:x86架构为什么需要内存屏障?

答案: 虽然x86是强内存模型(天然保证load-load有序性),但仍需:

  • StoreLoad屏障:防止store操作与后续load操作重排序(唯一需要显式插入的屏障)
  • Lock前缀指令:如volatile写实际编译为lock addl $0,0(%rsp),隐含完整屏障

最佳实践

  1. 优先使用java.util.concurrent工具类(已正确实现内存语义)
  2. 调试工具推荐:
    • -XX:+PrintAssembly查看汇编指令
    • jconsole观察线程内存状态
  3. 避免过度优化:99%的场景不需要手动插入内存屏障