Java 内存模型(Java Memory Model,JMM)是 JVM 定义的一套抽象规范,而非物理内存结构(如堆、栈、方法区)—— 它解决的是多线程并发场景下,因 CPU 缓存、指令重排序、多核处理器等硬件特性导致的内存访问一致性问题,核心目标是定义线程与主内存之间的交互规则,保障多线程程序的内存可见性、原子性、有序性。

一、先澄清:JMM ≠ JVM 内存结构

很多人会混淆「Java 内存模型(JMM)」和「JVM 运行时数据区(堆、栈、方法区等)」,两者的核心区别:

维度 Java 内存模型(JMM) JVM 运行时数据区
本质 抽象规范(解决多线程内存一致性问题) 物理内存划分(管理对象、方法、线程栈等存储)
关注对象 线程、主内存、工作内存的交互规则 堆(共享)、虚拟机栈(私有)、方法区等物理区域
核心目标 保障并发正确性(可见性、原子性、有序性) 内存分配与回收
二、JMM 的核心概念:主内存 & 工作内存

JMM 抽象出「主内存」和「工作内存」两个概念,所有多线程内存交互都基于这两个区域的规则:

  1. 主内存(Main Memory)

    • 所有线程共享的内存区域,存储程序中所有的共享变量(实例变量、静态变量、数组元素等,局部变量/方法参数是线程私有,不存于主内存);
    • 是共享变量的「唯一真实数据源」。
  2. 工作内存(Working Memory)

    • 每个线程私有的内存区域,存储线程对主内存共享变量的「副本」;
    • 线程对共享变量的所有操作(读、写)必须先加载到工作内存,操作完成后再写回主内存,不能直接操作主内存的变量
  3. JMM 定义的变量交互规则(8 种原子操作):
    JMM 规定了线程操作共享变量的 8 种基本操作,且每种操作必须满足原子性、可见性约束:

    • read:从主内存读取变量到工作内存;
    • load:将 read 读取的变量值加载到工作内存的变量副本中;
    • use:将工作内存的变量值传递给线程的执行引擎(如计算);
    • assign:将执行引擎的结果赋值给工作内存的变量副本;
    • store:将工作内存的变量值写入主内存;
    • write:将 store 写入的变量值更新到主内存的变量中;
    • lock:锁定主内存的变量(仅能被一个线程锁定);
    • unlock:解锁主内存的变量。
三、JMM 要解决的三大核心问题

多线程并发时,仅靠「主内存-工作内存」的基础规则无法保证正确性,因为会出现可见性、原子性、有序性 问题,JMM 通过规范和关键字解决这些问题。

问题1:可见性

定义

一个线程修改了共享变量的值,其他线程能「立刻看到」这个修改后的值。

产生原因

线程修改共享变量时,先修改工作内存的副本,并未立即写回主内存;其他线程仍读取自己工作内存的旧副本,导致“看不到”最新值。

JMM 的解决方式
  • volatile:强制变量的写操作立刻刷回主内存,读操作直接从主内存加载(跳过工作内存缓存),保证可见性;
  • synchronized:解锁时会将工作内存的修改刷回主内存,加锁时会清空工作内存,重新从主内存加载变量;
  • final:被 final 修饰的变量初始化完成后,对所有线程可见(不可修改,天然可见)。
代码示例:volatile 保证可见性
/**
 * 可见性问题演示:不加 volatile,线程2看不到线程1修改的 flag
 */
public class VisibilityDemo {
    // 共享变量:不加 volatile 时,线程2无法感知修改
    private static volatile boolean flag = false; 

    public static void main(String[] args) throws InterruptedException {
        // 线程1:修改 flag 为 true
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            System.out.println("线程1:flag 已改为 true");
        }).start();

        // 线程2:循环读取 flag,直到为 true 才退出
        new Thread(() -> {
            while (!flag) {
                // 不加 volatile 时,此处会无限循环(线程2读取的是工作内存的旧值)
                // 加 volatile 后,能立刻看到线程1的修改,退出循环
            }
            System.out.println("线程2:感知到 flag 为 true,退出循环");
        }).start();
    }
}
  • 不加 volatile:线程2会无限循环(工作内存缓存旧值);
  • volatile:线程1修改后立刻刷回主内存,线程2读取主内存最新值,退出循环。

问题2:原子性

定义

一个操作(或多个操作)要么全部执行且不被中断,要么完全不执行,中间状态不会被其他线程感知。

产生原因

JMM 仅保证「基本数据类型的读取/赋值(如 int a = 10)」是原子操作,但复合操作(如 i++a += b 拆分为多个步骤,会被线程切换打断。

JMM 的解决方式
  • synchronized:通过加锁保证代码块的原子性(同一时间仅一个线程执行);
  • java.util.concurrent.atomic 原子类(如 AtomicInteger):基于 CAS 指令实现无锁原子操作;
  • Lock 锁(如 ReentrantLock):手动控制锁的获取/释放,保证原子性。
代码示例:i++ 的原子性问题
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 原子性问题演示:i++ 是非原子操作,多线程下结果错误
 */
public class AtomicityDemo {
    // 普通变量:多线程 i++ 会出错
    private static int count = 0;
    // 原子类:保证自增操作原子性
    private static AtomicInteger atomicCount = new AtomicInteger(0);

    // 非原子操作:i++ = 读取count + 加1 + 写回count
    public static void increment() {
        count++;
    }

    // 原子操作:CAS 实现自增
    public static void atomicIncrement() {
        atomicCount.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        // 1000个线程,每个执行1000次 increment
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increment();
                    atomicIncrement();
                }
            }).start();
        }

        // 等待所有线程执行完成
        Thread.sleep(2000);
        System.out.println("普通count结果:" + count); // 小于1000000(原子性问题)
        System.out.println("原子类count结果:" + atomicCount); // 等于1000000(原子性保证)
    }
}
  • count++ 拆分为「读、加、写」三步,线程切换会导致值被覆盖,结果小于 1000000;
  • AtomicIntegerincrementAndGet() 基于 CAS 实现原子自增,结果准确。

问题3:有序性

定义

程序执行顺序「按代码书写顺序执行」,但编译器/CPU 为优化性能会进行「指令重排序」,单线程下重排序不影响结果,多线程下会导致执行顺序混乱。

产生原因
  • 编译器重排序:Java 编译器在不改变单线程语义的前提下,调整语句执行顺序;
  • CPU 重排序:CPU 为提升并行度,调整指令执行顺序。
JMM 的解决方式
  • volatile:禁止指令重排序(通过内存屏障实现);
  • synchronized:保证代码块内的执行顺序与代码顺序一致;
  • final:禁止 final 变量的写操作与构造函数重排序(避免半初始化对象)。
代码示例:DCL 单例的有序性问题
/**
 * 有序性问题演示:DCL单例(Double Check Lock)不加 volatile 的风险
 */
public class Singleton {
    // 不加 volatile:可能出现半初始化对象(指令重排序导致)
    private static volatile Singleton instance; 

    private Singleton() {}

    public static Singleton getInstance() {
        // 第一次检查:避免不必要的锁
        if (instance == null) {
            synchronized (Singleton.class) {
                // 第二次检查:防止多线程同时进入第一个if
                if (instance == null) {
                    // new Singleton() 拆分为3步:
                    // 1. 分配内存  2. 初始化对象  3. 赋值给instance
                    // 不加 volatile 时,CPU可能重排序为 1→3→2,导致其他线程读取到“半初始化对象”
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 不加 volatilenew Singleton() 被重排序为「分配内存→赋值→初始化」,线程A执行到「赋值」但未初始化时,线程B第一次检查 instance != null,直接返回半初始化的对象,导致空指针;
  • volatile:禁止 instance 的写操作重排序,保证「初始化完成后才赋值」。
四、JMM 的核心原则:Happens-Before(先行发生原则)

Happens-Before 是 JMM 定义的「可见性规则」—— 若操作 A 先行发生于操作 B,则 A 的结果对 B 可见,且 A 的执行顺序早于 B。它是判断多线程操作是否存在竞争的核心依据,无需关注底层重排序/缓存,只需判断是否满足以下规则:

规则名称 规则说明
程序次序规则 单线程内,前面的代码操作先行发生于后面的代码操作(按代码顺序)
管程锁定规则 对一个锁的 unlock 操作先行发生于后续对该锁的 lock 操作
volatile变量规则 volatile 变量的写操作先行发生于后续对该变量的读操作
线程启动规则 线程的 start() 方法先行发生于线程内的所有操作
线程终止规则 线程内的所有操作先行发生于线程的 join() 方法返回
传递性规则 若 A 先行于 B,B 先行于 C,则 A 先行于 C
对象终结规则 对象的构造函数执行完成先行发生于其 finalize() 方法执行
示例:Happens-Before 规则的应用
public class HappensBeforeDemo {
    private static int a = 0;
    private static volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        // 线程1:写操作
        Thread t1 = new Thread(() -> {
            a = 1; // 操作1
            flag = true; // 操作2(volatile写)
        });

        // 线程2:读操作
        Thread t2 = new Thread(() -> {
            if (flag) { // 操作3(volatile读)
                System.out.println(a); // 操作4:必输出1
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}
  • 程序次序规则:操作1 先行于 操作2;
  • volatile变量规则:操作2 先行于 操作3;
  • 传递性规则:操作1 先行于 操作4;
  • 因此,线程2读取到 flag=true 时,a=1 对线程2可见,输出1。
五、JMM 的关键细节补充
  1. long/double 的非原子性协定
    JMM 允许 JVM 将 64 位的 long/double 变量的读写拆分为两个 32 位操作(读高32位、读低32位),导致多线程下可能读取到“半个值”。但现代主流 JVM(如 HotSpot)已默认保证 long/double 的原子性,无需额外处理。

  2. 内存屏障(Memory Barrier)
    JMM 通过内存屏障实现 volatile 的禁止重排序和可见性:

    • volatile 后插入「写屏障」:强制刷回主内存,禁止后续指令重排序到写屏障前;
    • volatile 前插入「读屏障」:清空工作内存,从主内存加载最新值,禁止前面指令重排序到读屏障后。
六、总结
  1. JMM 是抽象规范,核心目标是解决多线程的可见性、原子性、有序性问题,而非管理物理内存;
  2. 核心概念是「主内存(共享)」和「工作内存(私有)」,线程操作共享变量必须通过工作内存;
  3. volatile 解决可见性+有序性,synchronized/原子类解决原子性,Happens-Before 是可见性的核心规则;
  4. 理解 JMM 的本质:通过约束线程与主内存的交互,屏蔽硬件层面的缓存、重排序差异,保证多线程程序的跨平台一致性。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐