# 什么是 Java 内存模型(JMM)?
10 min read
Table of Contents
什么是 Java 内存模型(JMM)?
Java 内存模型(JMM)面试标准答案
【核心定义】
Java 内存模型(JMM)是一个规范,它定义了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量的底层细节,其核心目标是解决多线程环境下,由于内存可见性、指令重排序等问题导致的并发安全问题。
【关键要点】
- 抽象的内存结构:JMM将内存抽象为主内存和工作内存。主内存存储所有共享变量,每个线程拥有自己私有的工作内存,存储该线程使用到的共享变量的副本。线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存。
- 内存间交互操作:JMM定义了8种原子性操作来规范主内存与工作内存之间的交互:
lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)。这些操作必须满足一系列规则,如read/load、store/write必须成对出现。 - 三大核心特性:
- 原子性:由JMM直接保证的原子性操作包括
read、load、assign、use、store、write。对于更大范围的原子性保证(如i++),需要依赖synchronized或Lock。 - 可见性:指一个线程修改了共享变量的值,其他线程能够立即得知这个修改。JMM通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现,具体依赖
volatile、synchronized、final关键字。 - 有序性:指程序执行的顺序按照代码的先后顺序执行。JMM允许编译器和处理器进行指令重排序以优化性能,但提供了
volatile、synchronized和happens-before规则来保证线程间的有序性。
- 原子性:由JMM直接保证的原子性操作包括
【深度推导/细节】
核心矛盾:硬件性能优化(如高速缓存、指令重排序)与程序并发正确性之间的矛盾。
逻辑拆解:
- 可见性问题根源:现代CPU有多级缓存,线程的工作内存可类比CPU缓存。当一个线程更新了缓存中的副本,若未写回主内存,或其他线程的缓存未失效,就会导致脏读。
- 有序性问题根源:编译器和处理器会进行指令重排序(在不改变单线程程序执行结果的前提下)。但在多线程环境下,重排序可能导致意想不到的结果。
- 经典案例:双重检查锁定(DCL)与指令重排序
if (instance == null) { // Step 1: 第一次检查synchronized (Singleton.class) { // Step 2: 加锁if (instance == null) { // Step 3: 第二次检查instance = new Singleton(); // Step 4: 问题所在!}}}
instance = new Singleton();这行代码并非原子操作,它可能被重排序为:- Step 1: 分配内存空间
- Step 2: 将内存地址赋值给
instance引用 (此时instance不为null,但对象尚未初始化!) - Step 3: 初始化对象
若线程A执行完Step 2后发生上下文切换,线程B在第一次检查时发现
instance不为null,便会直接返回一个未初始化完成的对象,导致程序错误。解决方法是使用volatile修饰instance,禁止相关重排序。
- 经典案例:双重检查锁定(DCL)与指令重排序
- JMM的解决方案:通过
happens-before规则(一组偏序关系)来定义“可见性”和“有序性”的保证。只要操作Ahappens-before操作B,那么A的结果对B可见,且A的执行顺序排在B之前。
【关联/对比】
- JMM vs JVM运行时内存区域:
- JVM内存区域(堆、栈、方法区)描述的是数据存储的物理或逻辑区域,是“静态”的。
- JMM描述的是多线程环境下,变量访问的规则和约定,是“动态”的、抽象的。工作内存涵盖了栈、寄存器甚至缓存,主内存主要对应堆中的共享变量部分。
synchronizedvsvolatile:synchronized:同时保证原子性、可见性、有序性。通过锁的获取与释放,隐式包含了对主内存的同步操作(进入同步块前清空工作内存,退出前将变量刷回主内存)。volatile:只保证可见性和有序性(禁止指令重排序),不保证原子性。其实现原理是在汇编指令中加入内存屏障(Memory Barrier)。
final关键字的特殊规则:被final修饰的字段,在构造器中被正确初始化后(没有this引用逃逸),对其他线程是立即可见的,无需同步。
『面试官追问』
-
happens-before原则具体包括哪些?- 程序次序规则:一个线程内,书写在前面的操作
happens-before后面的操作。 - 管程锁定规则:一个
unlock操作happens-before后续对同一个锁的lock操作。 volatile变量规则:对一个volatile变量的写操作happens-before后续对这个变量的读操作。- 线程启动规则:
Thread.start()happens-before此线程的每一个动作。 - 线程终止规则:线程中的所有操作都
happens-before对此线程的终止检测。 - 线程中断规则:对线程
interrupt()的调用happens-before被中断线程检测到中断事件。 - 对象终结规则:一个对象的初始化完成
happens-before它的finalize()方法的开始。 - 传递性:如果A
happens-beforeB,且Bhappens-beforeC,那么Ahappens-beforeC。
- 程序次序规则:一个线程内,书写在前面的操作
-
volatile如何保证可见性?底层原理是什么?- 当写一个
volatile变量时,JVM会向处理器发送一条Lock前缀的指令,这个指令有两个关键作用:- 将当前处理器缓存行的数据立即写回系统内存。
- 这个写回操作会使其他CPU里缓存了该内存地址的数据无效(缓存一致性协议,如MESI)。
- 因此,后续其他线程在读取该变量时,会发现自己的缓存无效,会重新从主内存中读取最新值。
- 当写一个
-
什么是内存屏障?
volatile如何通过内存屏障禁止重排序?- 内存屏障是一组CPU指令,用于控制特定条件下的重排序和内存可见性问题。
- JVM为
volatile读写插入以下屏障:- 在每个
volatile写操作前插入StoreStore屏障,之后插入StoreLoad屏障。 - 在每个
volatile读操作后插入LoadLoad屏障和LoadStore屏障。
- 在每个
- 这些屏障阻止了屏障两侧的指令进行重排序,并强制刷新缓存。
-
为什么
volatile不能保证原子性?举例说明。volatile的读写操作本身是原子的,但复合操作(如i++)不是。i++实际上分为read、increment、assign三个步骤,volatile只能保证每个步骤的可见性,但不能保证这三个步骤作为一个整体不被其他线程打断。- 解决方案:使用
java.util.concurrent.atomic包下的原子类(基于CAS),或使用synchronized。
-
从JDK 5开始,
volatile的语义被增强,具体增强了什么?- 在旧的JMM(JDK 5之前)中,
volatile变量并不能完全防止指令重排序。 - JDK 5使用新的JSR-133内存模型,严格限制了
volatile变量与普通变量的重排序,并正式确立了happens-before关系,使volatile的语义(尤其是禁止重排序的语义)变得真正严格和有效。这也是DCL问题必须用JDK 5及以上版本的volatile才能正确解决的原因。
- 在旧的JMM(JDK 5之前)中,