Java内存模型(JMM)速览

注意
本文最后更新于 2024-05-06,文中内容可能已过时。

Java内存模型(JMM)是Java并发编程的“宪法”,它不是物理层面的内存划分,而是一组抽象的规则和规范,用于解决多线程环境下共享变量的访问一致性问题。

一、JMM的核心目标

屏蔽不同硬件(CPU缓存架构)和操作系统对内存访问的差异,保证Java程序在所有平台上都能获得一致的内存访问效果。

二、JMM的三大核心问题

并发编程的三大核心问题,本质上都是由于JMM中“主内存-工作内存”的交互机制导致的。

  1. 可见性(Visibility)
  • 问题:当一个线程修改了共享变量的值,其他线程不能立即感知到该修改。
  • 原因:线程将变量缓存在自己的工作内存中,修改后未立即同步回主内存。
  • 解决方案:volatilesynchronizedfinal
  1. 原子性(Atomicity)
  • 问题:一个或多个操作在执行过程中被中断,导致执行结果不符合预期。
  • 经典案例:i++不是原子操作,它包含“读取→计算→写回”三个步骤。
  • 解决方案:synchronizedLockAtomicInteger等原子类。
  1. 有序性(Ordering)
  • 问题:为了优化性能,编译器和处理器会对指令进行重排序,导致多线程下的执行顺序与代码顺序不一致。
  • 解决方案:volatile(通过内存屏障禁止重排序)、synchronized(保证同一时刻只有一个线程执行)。

三、JMM的内存抽象:主内存与工作内存

JMM将所有变量存储在主内存中,每个线程拥有自己的工作内存。

  1. 主内存(Main Memory)
  • 所有线程共享的内存区域,存储所有实例变量、静态变量等共享变量。
  • 物理上大致对应于RAM(随机存取存储器)。
  1. 工作内存(Working Memory)
  • 每个线程私有的内存区域,存储该线程使用的共享变量的副本。
  • 物理上大致对应于CPU的寄存器、L1/L2缓存。
  • 线程对变量的所有操作(读、写)都必须在工作内存中进行,不能直接读写主内存。
  1. 线程间通信机制
  • 变量值的传递必须通过主内存完成:线程A修改变量→刷新到主内存→线程B从主内存读取→加载到自己的工作内存。
  1. 八种原子操作 JMM定义了8种原子操作来实现主内存和工作内存的交互:
  • read(读取):从主内存传输变量到工作内存。
  • load(载入):把read得到的值放入工作内存的变量副本中。
  • use(使用):把工作内存中的变量值传递给执行引擎。
  • assign(赋值):将执行引擎接收到的值赋给工作内存变量。
  • store(存储):把工作内存中的变量值传送到主内存。
  • write(写入):把store得到的值放入主内存的变量中。
  • lock(锁定):标识主内存中的变量为线程独占。
  • unlock(解锁):释放主内存中变量的锁定状态。

四、JMM的核心规则:Happens-Before原则

Happens-Before是JMM判断数据是否存在竞争、线程是否安全的核心规则。如果操作A Happens-Before操作B,那么A的执行结果对B是可见的,且A的执行顺序排在B之前。

  1. 程序顺序规则
  • 同一个线程中,按照代码顺序,前面的操作happens-before后面的操作。
  1. 监视器锁规则
  • 对一个锁的解锁,happens-before于随后对这个锁的加锁
  1. volatile变量规则
  • 对一个volatile域的,happens-before于任意后续对这个volatile域的
  1. 线程启动规则
  • Thread.start()的调用,happens-before于该线程中的任何操作。
  1. 线程终止规则
  • 线程中的所有操作,happens-before于其他线程检测到该线程已经终止(如join()返回)。
  1. 中断规则
  • 对线程interrupt()的调用,happens-before于被中断线程检测到中断事件。
  1. 传递性
  • 如果A Happens-Before B,且B Happens-Before C,则A Happens-Before C。

五、JMM的实现机制

  1. volatile关键字
  • 保证可见性:写操作立即刷新到主内存,读操作立即从主内存加载。
  • 禁止重排序:通过插入内存屏障(Memory Barrier)禁止编译器和处理器对volatile变量的读写操作进行重排序。
  • 不保证原子性:复合操作(如v++)仍需配合锁使用。
  1. synchronized关键字
  • 保证原子性:通过独占锁保证同一时刻只有一个线程执行临界区代码。
  • 保证可见性:解锁前必须将工作内存中的变量同步回主内存,加锁时必须从主内存重新加载变量。
  • 保证有序性:通过“同一时刻只有一个线程执行”保证有序性。
  1. final关键字
  • 构造完成后,final字段对其他线程可见,且不可修改。

六、JMM与JVM内存模型的区别

对比维度 JMM JVM内存模型
核心范畴 并发编程的理论基石 JVM的运行时内存结构
主要目标 解决多线程环境下的可见性、原子性、有序性问题 定义程序运行时数据的存储、分配和管理
抽象概念 主内存、工作内存 堆、虚拟机栈、方法区等
具体实现 volatile、synchronized、final等关键字的语义 具体的内存区域划分,如堆内存、栈帧等
关注点 线程如何与内存交互,保证线程安全 内存如何分配、回收,以及对象的生命周期

七、案例:DCL单例模式为何需要volatile

双重检查锁(DCL)单例模式必须加volatile,否则可能导致其他线程获取到未初始化完成的实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Singleton {
    // 必须加volatile,保证可见性和禁止重排序
    private static volatile Singleton instance;
    
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    // 对象初始化分为三步:
                    // 1. 分配内存空间
                    // 2. 初始化对象(调用构造方法)
                    // 3. 将instance指向内存地址
                    // 如果没有volatile,指令重排序可能导致1→3→2
                    // 此时其他线程看到instance不为null(3已执行),但对象还没初始化(2未执行),就会报错。
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}