一、JMM核心概念
1. 主内存与工作内存
JMM抽象模型:
- 主内存:所有线程共享的内存区域,存储对象实例、静态变量、数组等
- 工作内存:每个线程独占的内存区域,存储主内存中变量的副本
- 交互规则:线程对变量的操作(读取、赋值)必须在工作内存中进行,不能直接操作主内存
内存交互操作:
read:将主内存变量读取到工作内存load:将read的变量值存入工作内存的变量副本use:将工作内存变量值传递给执行引擎assign:将执行引擎结果赋值给工作内存变量store:将工作内存变量值写入主内存write:将store的变量值存入主内存的变量中
2. 三大特性定义
| 特性 | 定义 | 表现 |
|---|---|---|
| 原子性 | 一个操作或多个操作要么全部执行完成,要么全部不执行 | i++ 不是原子操作(包含read/load/use/assign/store/write) |
| 可见性 | 一个线程对变量的修改,其他线程能立即看到 | 线程A修改x=1,线程B可能看不到 |
| 有序性 | 程序执行的顺序按照代码的先后顺序执行 | 编译器/处理器可能重排序指令,导致执行顺序与代码顺序不一致 |
3. 三大特性实现机制(面试重点:JMM如何保证原子性、可见性、有序性?)
原子性实现:
- synchronized关键字 :
- 原理:通过监视器锁实现,进入同步块获取锁,退出同步块释放锁
- 范围:同步块内的所有操作具有原子性
- Lock接口 :
- 如
ReentrantLock,原理类似synchronized,但提供更灵活的锁操作
- 如
- 原子类 :
- 如
AtomicInteger,基于CAS操作实现,保证单个变量的原子更新 - 适用场景:简单变量的原子操作,性能优于锁
- 如
可见性实现:
- volatile关键字 :
- 原理:
- 写操作:修改工作内存变量后,立即刷新到主内存
- 读操作:读取工作内存变量前,先从主内存刷新最新值
- 作用:保证变量的可见性,禁止指令重排序
- 原理:
- synchronized关键字 :
- 原理:释放锁时,将工作内存变量刷新到主内存
- 作用:同步块结束时,保证变量的可见性
- final关键字 :
- 原理:final变量初始化完成后,值不能修改,且对其他线程可见
- 作用:保证final变量的可见性
有序性实现:
- volatile关键字 :
- 原理:通过内存屏障禁止指令重排序
- 作用:保证volatile变量前后的指令不会被重排序
- synchronized关键字 :
- 原理:同步块内的操作作为一个整体执行,不会被重排序
- 作用:保证同步块内的操作有序性
- happens-before原则 :
- 原理:通过规则定义操作间的顺序关系
- 作用:无需额外同步,保证操作的有序性
二、happens-before原则
1. 定义
happens-before原则是JMM定义的线程间操作的顺序关系 ,用于保证多线程环境下的可见性 和有序性。如果操作A happens-before操作B,那么A的执行结果对B可见,且A的执行顺序在B之前。
2. 8条规则(面试重点:请列举happens-before原则的几条重要规则)
1. 程序顺序规则:
-
同一线程内,按照代码顺序,前面的操作happens-before后面的操作
-
示例:
javaint a = 1; // 操作A int b = a + 1; // 操作B // A happens-before B,B能看到A的结果
2. 监视器锁规则:
-
对同一个锁的解锁操作 happens-before后续对该锁的加锁操作
-
示例:
javasynchronized (lock) { // 加锁 x = 1; // 操作A } // 解锁(操作B) synchronized (lock) { // 加锁(操作C) y = x; // 操作D,能看到A的结果 } // B happens-before C,D能看到A的结果
3. volatile变量规则:
-
对volatile变量的写操作 happens-before后续对该变量的读操作
-
示例:
javavolatile int x = 0; Thread A: x = 1; // 操作A(写volatile) Thread B: int y = x; // 操作B(读volatile),y=1 // A happens-before B,B能看到A的结果
4. 线程启动规则:
-
Thread.start()操作happens-before线程内的任何操作 -
示例:
javaThread t = new Thread(() -> { System.out.println(x); // 能看到x=1 }); x = 1; // 操作A t.start(); // 操作B // A happens-before B,线程内操作能看到A的结果
5. 线程终止规则:
- 线程内的所有操作happens-before其他线程检测到该线程终止
- 示例:通过
Thread.join()或Thread.isAlive()检测线程终止
6. 线程中断规则:
- 对线程的
interrupt()操作happens-before被中断线程检测到中断事件 - 示例:通过
Thread.interrupted()检测中断
7. 对象终结规则:
- 对象的构造函数执行完成happens-before对象的
finalize()方法执行 - 示例:对象初始化完成后,finalize()方法才能执行
8. 传递性规则:
- 若A happens-before B,B happens-before C,则A happens-before C
- 示例:结合volatile变量规则和程序顺序规则,保证跨线程操作的可见性
3. 作用
- 简化同步编程:无需显式同步,通过规则保证可见性和有序性
- 指导编译器优化:编译器在不违反happens-before原则的前提下,可以进行重排序
- 保证多线程正确性:确保线程间操作的顺序关系,避免数据竞争
三、指令重排序
1. 定义
指令重排序是指编译器或处理器为了优化性能,改变指令的执行顺序,但不改变程序的语义(单线程下的执行结果不变)。
2. 分类
1. 编译器重排序:
-
发生阶段:编译时,由编译器优化
-
目的:提高指令执行效率,减少CPU空闲时间
-
示例 :
java// 代码顺序 int a = 1; // 操作A int b = 2; // 操作B // 编译器可能重排序为 B → A,单线程结果不变 -
解决方法 :编译器内存屏障(如
volatile关键字禁止重排序)
2. 处理器重排序:
-
发生阶段:CPU执行时,由处理器优化
-
类型 :
- 指令级并行重排序:CPU流水线并行执行指令
- 乱序执行引擎重排序:CPU动态调整指令执行顺序
-
示例 :
java// 代码顺序 int a = 1; // 操作A(依赖内存) int b = a + 1; // 操作B(依赖A) int c = 2; // 操作C(无依赖) // 处理器可能重排序为 A → C → B,因为C无依赖,可并行执行 -
解决方法 :处理器内存屏障(如
lfence、sfence、mfence指令)
3. 内存重排序:
-
发生阶段:内存访问时,由于缓存一致性协议导致
-
表现 :
- 写缓冲:CPU写操作先存入写缓冲,稍后刷入主内存
- 无效队列:CPU收到失效消息,先存入无效队列,稍后处理
-
示例 :
java// 线程A x = 1; // 写x(存入写缓冲) int r1 = y; // 读y(可能读到旧值) // 线程B y = 1; // 写y(存入写缓冲) int r2 = x; // 读x(可能读到旧值) // 可能出现r1=0且r2=0的情况,这是内存重排序导致的 -
解决方法:内存屏障,强制刷新写缓冲或清空无效队列
3. 内存屏障(解决重排序的关键)
定义 :内存屏障是一条指令,用于禁止特定类型的指令重排序,并保证内存可见性。
分类:
| 屏障类型 | 作用 | 示例指令 |
|---|---|---|
| LoadLoad Barriers | 禁止LOAD1 → LOAD2重排序 | lfence |
| StoreStore Barriers | 禁止STORE1 → STORE2重排序 | sfence |
| LoadStore Barriers | 禁止LOAD1 → STORE2重排序 | |
| StoreLoad Barriers | 禁止STORE1 → LOAD2重排序(最强屏障) | mfence |
volatile的内存屏障实现:
- 写操作后:插入StoreStore屏障 + StoreLoad屏障
- 读操作前:插入LoadLoad屏障 + LoadStore屏障
synchronized的内存屏障实现:
- 进入同步块:插入LoadLoad屏障 + LoadStore屏障
- 退出同步块:插入StoreStore屏障 + StoreLoad屏障
四、面试题解答
1. JMM如何保证原子性、可见性、有序性?
原子性:
- 通过synchronized 和Lock接口保证:同步块内的操作作为一个整体执行,不可中断
- 通过原子类保证:基于CAS操作,保证单个变量的原子更新
可见性:
- 通过volatile保证:写操作立即刷新到主内存,读操作先从主内存刷新
- 通过synchronized保证:释放锁时刷新到主内存,获取锁时从主内存读取
- 通过final保证:final变量初始化完成后,对其他线程可见
有序性:
- 通过volatile保证:禁止指令重排序,确保volatile变量前后的指令顺序
- 通过synchronized保证:同步块内的操作作为一个整体,不会被重排序
- 通过happens-before原则保证:无需显式同步,通过规则定义操作间的顺序关系
2. 请列举happens-before原则的几条重要规则。
重要规则:
- 程序顺序规则:同一线程内,代码顺序决定操作顺序
- 监视器锁规则:解锁happens-before后续加锁
- volatile变量规则:写volatile happens-before后续读volatile
- 线程启动规则:start() happens-before线程内操作
- 传递性规则:A happens-before B,B happens-before C → A happens-before C
五、总结
Java内存模型(JMM)是Java并发编程的基础,通过主内存/工作内存模型 、三大特性 、happens-before原则 和指令重排序等概念,保证多线程环境下的程序正确性。理解JMM的核心机制,对于编写高效、安全的并发程序至关重要,也是面试中的高频考点。
核心要点:
- JMM通过synchronized 、volatile 、原子类等机制保证原子性、可见性、有序性
- happens-before原则定义了线程间操作的顺序关系,简化同步编程
- 指令重排序 是性能优化的结果,通过内存屏障解决其带来的并发问题
掌握JMM的原理,能够帮助开发者理解并发问题的根源,选择合适的同步机制,编写高效、可靠的并发程序。