一、JMM内存模型

JMM是一种抽象规范,并不对应物理内存,而是定义了线程和内存之间的交互规则。它的核心设计如下:

概念 作用
主内存(Main Memory) 所有共享变量(实例字段、静态字段等)存储的地方,是所有线程共享的公共区域。
工作内存(Working Memory) 每个线程私有的内存区域,存储该线程使用的共享变量的副本(从主内存拷贝)。

并发三特性:

  • 原子性

  • 可见性 (读)

  • 有序性

二、JMM解决的三大核心问题

1. 可见性(Visibility)

问题:一个线程修改了共享变量,其他线程可能无法立即感知。

JMM解决方案

volatile关键字:保证变量的修改会立即刷新到主内存,且其他线程读取时会强制从主内存重新加载(禁止缓存副本)。

**synchronized/Lock**:解锁前必须将变量写回主内存,加锁时会清空工作内存并从主内存重新加载。

面试考点volatile不保证原子性,仅保证可见性和有序性(部分场景)。

2. 原子性(Atomicity)

问题:某些操作(如 i++)看似一步,实际分为“读取-修改-写入”三步,多线程下可能打断。

JMM解决方案

synchronized代码块/方法:通过监视器锁(Monitor)保证同一时间只有一个线程执行临界区代码。

java.util.concurrent.atomic:如 AtomicInteger,基于CAS(Compare-And-Swap)硬件指令实现无锁原子操作。

面试考点i++为什么不是原子的?如何用 AtomicInteger解决?

3. 有序性(Ordering)

问题:编译器和处理器会对指令重排序(优化性能),可能导致多线程下逻辑错误。

JMM解决方案

volatile禁止重排序:对 volatile变量的写操作前的代码不会被重排序到写操作后;读操作后的代码不会被重排序到读操作前。

synchronized天然有序性:同一时刻只有一个线程执行同步块,相当于单线程执行(但同步块内的代码仍可能被重排序)。

经典案例:双重检查锁定(DCL)的单例模式中,instance必须声明为 volatile,否则可能因重排序导致返回未初始化的对象。


三、Happens-Before 原则(面试核心)

JMM通过 Happens-Before(先行发生)规则 定义操作间的可见性关系,无需依赖 volatile/synchronized也能判断线程安全。以下是8条核心规则(重点记前4条):

规则 含义
程序次序规则 同一个线程内,按代码顺序,前面的操作 Happens-Before 后面的操作(逻辑上)。
volatile变量规则 volatile变量的写操作 Happens-Before 后续对该变量的读操作。
synchronized规则 对一个锁的解锁 Happens-Before 后续对这个锁的加锁。
线程启动规则 Thread.start()Happens-Before 该线程的所有后续操作。
线程终止规则 线程中的所有操作 Happens-Before 其他线程检测到该线程已终止(如 t.join()返回)。
中断规则 对线程 interrupt()的调用 Happens-Before 被中断线程检测到中断事件。
对象终结规则 对象的构造函数执行结束 Happens-Before 其 finalize()方法开始。
传递性规则 若 A Happens-Before B,且 B Happens-Before C,则 A Happens-Before C。

面试应用:判断两个操作是否线程安全,只需看是否满足 Happens-Before 规则。


四、JMM的实现:内存屏障(Memory Barriers)

==volatile是 Java 关键字,用于修饰变量;Happens-Before是该变量读写操作所遵循的抽象规则;而内存屏障则是 JVM 为了实现这些规则而在底层插入的具体指令==

屏障类型 作用
LoadLoad屏障 确保 Load1 数据的装载先于 Load2 及所有后续装载指令。
StoreStore屏障 确保 Store1 数据对其他处理器可见(刷新到内存)先于 Store2 及后续存储指令。
LoadStore屏障 确保 Load1 数据装载先于 Store2 及后续存储指令刷新到内存。
StoreLoad屏障 确保 Store1 数据刷新到内存先于 Load2 及后续装载指令(全能型,开销最大)。

volatile的屏障插入策略

写操作后插入 StoreStore+ StoreLoad

读操作前插入 LoadLoad+ LoadStore

“这三者在 JMM 中处于不同的层级,共同协作解决并发的可见性和有序性问题。

首先,**Happens-Before是 JMM 定义的一套抽象规则,它规定了某些操作之间必须具有可见性。比如其中有一条专门针对 volatile的规则:volatile变量的写操作 Happens-Before 于后续对这个变量的读操作**。

其次,**volatile是 Java 提供的关键字**,它是程序员触发这套规则的手段。当我们声明一个变量为 volatile时,就相当于要求 JVM 必须遵守上述的 HB 规则。

最后,内存屏障是具体的实现机制。JVM 为了在底层实现 volatile的 HB 语义,会在 volatile变量读写的前后插入特定的内存屏障指令(如写后插 StoreLoad屏障,读前插 LoadLoad屏障)。这些屏障会阻止指令重排序,并强制刷新处理器缓存,从而在硬件层面保证了 HB 规则的落地。”


五、面试高频问题与回答思路

1. 请解释JMM是什么?

:JMM是Java内存模型,定义了多线程环境下共享变量的访问规则,通过抽象主内存和工作内存,解决了并发编程的可见性、原子性和有序性问题,具体通过 volatilesynchronized、Happens-Before规则等实现。

2. volatilesynchronized的区别?

对比点 volatile synchronized
原子性 不保证(如 i++仍需锁) 保证(临界区代码原子执行)
可见性 保证(强制刷新主内存+禁用缓存) 保证(解锁前写回主内存,加锁时重载)
有序性 禁止指令重排序(部分) 保证(同步块内单线程语义)
性能 轻量级(无锁,开销小) 重量级(可能涉及线程阻塞/唤醒)

3. 什么是指令重排序?为什么要重排序?

:重排序是编译器或处理器为了优化性能,在不改变单线程语义的前提下,调整指令执行顺序。例如,int a=1; int b=2;可能被重排序为 b=2; a=1;。但多线程下可能导致逻辑错误(如DCL单例问题),需通过 volatile或锁禁止重排序。

4. 双重检查锁定的单例中,为什么 instance要加 volatile

:若不加 volatile,可能出现以下情况:线程A执行 instance = new Singleton()时,分为“分配内存→初始化对象→赋值引用”三步,其中“初始化对象”和“赋值引用”可能被重排序。此时线程B进入 getInstance(),发现 instance非空,直接返回一个未完全初始化的对象,导致错误。volatile禁止这种重排序,保证安全性。

==volatile禁止指令重排保证有序性==