Java内存模型(JMM)详解

1,052 阅读6分钟

Java内存模型(JMM)详解


一、JMM 是什么?

Java 内存模型(Java Memory Model, JMM)是一组规则,定义了多线程环境下对共享变量的访问方式,确保程序在不同平台和编译器优化下仍能正确运行。
核心目标:解决多线程并发中的 可见性有序性原子性 问题。


二、JMM 的核心概念

1. 主内存与工作内存

  • 主内存(Main Memory):所有线程共享的内存区域,存储共享变量的原始值。
  • 工作内存(Working Memory):每个线程私有的内存区域,存储共享变量的副本。
  • 交互规则
    • 线程对变量的操作(读/写)必须通过工作内存完成。
    • 线程间通信需将工作内存的值刷新到主内存,其他线程再从主内存读取。

2. 内存间的操作

JMM 定义了 8 种原子操作(基于 volatile 的变量有特殊规则):

操作说明
lock锁定主内存变量,标识为线程独占
unlock解锁变量,其他线程可访问
read从主内存读取变量到工作内存
loadread 的值放入工作内存的变量副本
use将工作内存的变量值传递给执行引擎
assign将执行引擎的值赋给工作内存的变量副本
store将工作内存的变量值传送到主内存
writestore 的值写入主内存变量

三、JMM 解决的三大问题

1. 可见性(Visibility)

  • 问题:一个线程修改共享变量后,其他线程无法立即看到修改。
  • JMM 解决方案
    • volatile 关键字:强制变量读写直接操作主内存。
    • synchronizedLock:通过锁的释放与获取同步主内存。
    • final 关键字:正确初始化后,对其他线程可见。

2. 有序性(Ordering)

  • 问题:编译器和处理器可能对指令重排序,导致执行顺序与代码不一致。
  • JMM 解决方案
    • volatile 关键字:禁止指令重排序(内存屏障)。
    • happens-before 规则:定义操作间的可见性顺序。

3. 原子性(Atomicity)

  • 问题:多线程操作共享变量时,中间状态可能被其他线程破坏。
  • JMM 解决方案
    • synchronizedLock:通过互斥锁保证代码块原子性。
    • 原子类(如 AtomicInteger:基于 CAS 实现无锁原子操作。

四、Happens-Before 规则

JMM 通过 happens-before 规则定义操作间的可见性顺序,确保以下场景的线程安全:

  1. 程序顺序规则
    单线程中,操作按代码顺序执行(as-if-serial 语义)。

  2. 锁规则
    解锁操作 happens-before 后续的加锁操作。

    synchronized (lock) {
        // 操作 A
    } // 解锁
    // 操作 B(能看到操作 A 的结果)
    
  3. volatile 变量规则
    volatile 变量 happens-before 后续读该变量。

    volatile boolean flag = false;
    
    // 线程 1
    flag = true; // 写操作
    
    // 线程 2
    if (flag) { // 读操作能看到线程 1 的写入
        // ...
    }
    
  4. 线程启动规则
    线程的 start() 调用 happens-before 该线程的任何操作。

    Thread t = new Thread(() -> System.out.println("Running"));
    t.start(); // start() happens-before 线程内的操作
    
  5. 线程终止规则
    线程的所有操作 happens-before 其他线程检测到该线程终止(如 t.join())。

    Thread t = new Thread(() -> { /* 操作 */ });
    t.start();
    t.join(); // 主线程能看到 t 的所有操作结果
    
  6. 传递性规则
    A happens-before B,且 B happens-before C,则 A happens-before C


五、volatile 关键字的内存语义

1. 可见性

  • 写操作:将工作内存的值刷新到主内存。
  • 读操作:从主内存重新加载最新值到工作内存。

2. 禁止指令重排序

  • 内存屏障:插入 LoadLoadLoadStoreStoreStoreStoreLoad 屏障。
  • 示例:解决双重检查锁定(DCL)单例模式的问题。
    public class Singleton {
        private static volatile Singleton instance;
        
        public static Singleton getInstance() {
            if (instance == null) { // 第一次检查
                synchronized (Singleton.class) {
                    if (instance == null) { // 第二次检查
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
    • volatile:对象的初始化可能被重排序,导致其他线程访问到未完全构造的对象。

六、synchronized 的内存语义

1. 锁的获取与释放

  • 进入同步块(加锁):将工作内存中的共享变量失效,从主内存重新加载。
  • 退出同步块(解锁):将工作内存的变量刷新到主内存。

2. 原子性与可见性

  • 原子性:同步块内的操作不可分割。
  • 可见性:锁释放前,所有修改对其他线程可见。

七、final 关键字的内存语义

  • 初始化安全性:正确构造的对象中,final 字段的值对其他线程可见,无需同步。
  • 禁止重排序:构造函数内对 final 字段的写入不会被重排序到构造函数外。

八、JMM 的常见问题与解决方案

1. 内存可见性问题

  • 场景:线程 A 修改变量后,线程 B 无法立即看到。
  • 解决:使用 volatile 或同步机制(如 synchronized)。

2. 指令重排序问题

  • 场景:单例模式中返回未完全初始化的对象。
  • 解决:用 volatile 修饰单例实例。

3. 原子性问题

  • 场景:多线程递增操作导致结果错误。
  • 解决:使用原子类(如 AtomicInteger)或同步块。

九、总结

JMM 的核心目标:在多线程环境中,通过定义内存访问规则,平衡性能与正确性。
关键实践

  • 使用 volatile 解决可见性和有序性问题。
  • 通过 synchronized 或原子类保证原子性。
  • 遵循 happens-before 规则设计线程安全代码。

理解 JMM 是编写高效、正确并发程序的基础,尤其在分布式系统和高性能计算场景中至关重要。

十、JMM 与 JVM 内存结构的区别

维度JMM(Java Memory Model)JVM 内存结构
目标定义多线程环境下的内存访问规则定义 JVM 运行时内存区域的物理划分
核心概念主内存、工作内存、happens-before堆、栈、方法区、程序计数器等
关注点线程间通信、可见性、有序性对象分配、垃圾回收、方法调用栈管理
开发者影响直接影响多线程编程的正确性影响内存优化、垃圾回收调优

十一、JMM 的实际应用

1. 双重检查锁单例模式(需 volatile 保证有序性)

public class Singleton {
    private static volatile Singleton instance; // 防止指令重排序

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 非原子操作,可能重排序
                }
            }
        }
        return instance;
    }
}
  • 问题:若无 volatile,其他线程可能拿到未初始化的对象。
  • 解决volatile 禁止指令重排序(JMM 有序性保障)。

2. 状态标志(volatile 保证可见性)

public class TaskRunner {
    private volatile boolean running = true;

    public void stop() {
        running = false; // 对其他线程立即可见
    }

    public void run() {
        while (running) { // 及时读取最新值
            // 执行任务
        }
    }
}

七、总结

  • JMM 是规范:定义线程与内存的交互规则,解决并发三性问题。
  • 核心机制
    • happens-before 规则保证有序性和可见性。
    • volatile、锁、原子类提供原子性和内存屏障。
  • 实际开发
    • 优先使用 volatile 解决可见性和有序性问题。
    • 复合操作使用锁或原子类保证原子性。
    • 理解 JMM 是编写正确、高效并发代码的基础。