Skip to content

Java 内存模型 JMM

什么是 JMM?

JMM(Java 内存模型)是 Java 规范中定义的多线程访问共享变量的规则集合,它屏蔽了不同 CPU 架构的内存差异,给 Java 程序员提供一个统一的内存视图。

JMM示例图

核心问题:线程 A 把 x 改成 42 写入自己的工作内存,但还没刷回主内存时,线程 B 读到的 x 仍是旧值 0。JMM 定义了什么时候必须刷新、什么时候可以看见对方的修改。

什么是可见性、原子性、有序性?

线程安全的三个核心维度,每个都对应不同的问题和解决方案:

原子性

一个操作要么全部执行,要么完全不执行,中间不被打断。

  • 单次读/写基本类型(long/double 除外)是原子的
  • i++ 不是原子的(读-改-写三步)
  • 解决:synchronized、AtomicInteger

可见性

一个线程对共享变量的修改,其他线程能立即看到最新值。

  • 问题根源:线程操作的是工作内存副本
  • CPU 缓存和指令重排都可能导致不可见
  • 解决:volatile、synchronized

有序性

程序执行的顺序符合代码书写的顺序,不被重排。

  • 编译器和 CPU 会对指令重排序以提高性能
  • 单线程内重排不影响结果,但多线程下会出问题
  • 解决:volatile(内存屏障)、synchronized
java
// 有序性问题经典案例 — 双重检查锁(DCL)
public class Singleton {
    // 必须加 volatile,禁止指令重排
    private static volatile Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        // 第一次检查:避免不必要的同步
        if (instance == null) {
            synchronized (Singleton.class) {
                // 第二次检查:防止多次创建
                if (instance == null) {
                    instance = new Singleton();
                    // new Singleton() 分三步:
                    // 1. 分配内存
                    // 2. 初始化对象(调用构造方法)
                    // 3. 将引用赋给 instance
                    // 
                    // 没有 volatile 时,可能重排为 1→3→2
                    // 线程 B 拿到 instance 时,对象还未初始化!
                }
            }
        }
        return instance;
    }
}

什么是 happens-before?

happens-before 是 JMM 中定义"操作可见性"的核心规则。如果操作 A happens-before 操作 B,则 A 的结果对 B 可见,且 A 的执行顺序在 B 之前。

它不是说 A 一定先于 B 物理执行,而是说"从 B 的视角看,A 的结果一定可见"。JMM 定义了 8 条基本规则:

程序顺序规则

同一线程中,前面的操作 happens-before 后面的操作。

java
int a = 1;  // 操作1
int b = a;  // 操作2 — 保证读到 a=1

volatile 变量规则

对一个 volatile 变量的写操作 happens-before 后续对它的读操作。

java
volatile boolean ready = false;

// 线程A写
ready = true;  // 写

// 线程B读
if (ready) { ... }  // 一定能读到 true

锁规则(监视器锁)

对一个锁的解锁(unlock)happens-before 后续对这个锁的加锁(lock)。

java
synchronized(lock) {
  x = 42;   // 写
}  // unlock

// 另一线程
synchronized(lock) {
  // lock 后,x 一定是 42
}

线程启动规则

Thread.start() happens-before 被启动线程中的任何操作。

java
x = 100;
t.start();  // start() 之前的写对新线程可见

// 新线程中
// x 一定是 100

线程终止规则

线程中所有操作 happens-before 其他线程检测到该线程终止(join() 返回等)。

java
// 线程T中
x = 200;

// 主线程
t.join();
// join() 返回后,x 一定是 200

线程中断规则

对线程调用 interrupt() happens-before 被中断线程检测到中断。

java
t.interrupt();

// 被中断线程中
// Thread.interrupted() 返回 true

对象终结规则

一个对象初始化完成(构造函数执行结束)happens-before 它的 finalize() 方法的开始。

java
// 构造完成 happens-before finalize()

传递性

若 A happens-before B,且 B happens-before C,则 A happens-before C。

java
// A hb B, B hb C → A hb C
// 这是推导复杂 happens-before 的基础

happens-before 是 JMM 对程序员的承诺——只要你的代码满足这些规则,JVM 保证内存可见性和有序性,不用关心底层 CPU 缓存细节。

volatile 为什么能保证可见性?

可见性问题的根源是 CPU 多级缓存:每个核心都有自己的 L1/L2 缓存,线程读写的是缓存中的副本,不会立即同步到主内存。

普通变量和volatile变量对比图

volatile 通过插入内存屏障(Memory Barrier)实现:

写操作:StoreStore + StoreLoad 屏障

写入 volatile 变量后,JVM 强制将值刷新到主内存,并让其他所有 CPU 核心的缓存行失效(Cache Invalidation)

读操作:LoadLoad + LoadStore 屏障

读取 volatile 变量前,JVM 强制从主内存重新加载最新值,跳过本地缓存副本

volatile 写 → 刷入主内存 + 使其他缓存失效;volatile 读 → 强制从主内存加载。底层依赖 CPU 的 MESI 缓存一致性协议和内存屏障指令(如 x86 的 LOCK 前缀)。

指令重排序是什么?

编译器和 CPU 为了提高执行效率,会在不影响单线程结果的前提下,对指令进行重新排序。但多线程下,这会导致另一个线程看到"中间状态"。

重排序如何在多线程中制造 Bug

1

双重检查锁为什么要加 volatile?

双重检查锁(DCL)是单例模式的经典写法,目的是减少锁竞争。但如果不加 volatile,在多线程下会因指令重排序出现严重 bug。

DCL 代码结构

java
if (instance == null) {          // 第一次检查(无锁)
  synchronized (Singleton.class) {
    if (instance == null) {      // 第二次检查(有锁)
      instance = new Singleton();
    }
  }
}
return instance;

new Singleton() 的真实步骤

// 步骤1:分配内存空间
// 步骤2:初始化对象
// 步骤3:将引用赋给 instance

CPU 可能把步骤2和3交换顺序,变成:分配内存 → 赋引用 → 初始化

重排序后,另一个线程会拿到"未初始化完成"的对象

写法代码问题
错误写法(无 volatile)private static Singleton instance;重排序可能导致其他线程拿到半初始化对象
正确写法(加 volatile)private static volatile Singleton instance;volatile 写屏障禁止步骤2和3重排,保证初始化完成后才赋引用

volatile 在 DCL 里的作用不是保证可见性(synchronized 已经保证了),而是禁止 new 操作内部的指令重排序,防止其他线程看到未初始化完成的对象。