JMM
一、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内存模型,定义了多线程环境下共享变量的访问规则,通过抽象主内存和工作内存,解决了并发编程的可见性、原子性和有序性问题,具体通过 volatile、synchronized、Happens-Before规则等实现。
2. volatile和 synchronized的区别?
| 对比点 | 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禁止指令重排保证有序性==
