Java 的内存模型(JMM)介绍
JMM 核心定义和作用
Java 内存模型(Java Memory Model,JMM)是 Java 虚拟机规范中定义的一种抽象概念,它规定了多线程环境下,线程如何与内存进行交互。
JMM 的核心作用:
- 定义程序中各个变量的访问规则
- 确保多线程程序的可见性、有序性和原子性
- 屏蔽不同硬件平台和操作系统的内存访问差异
JVM 和 JMM 的区别
说到 JMM,我们不得不提到它经常被人所搞混淆的另一个概念 JVM,我们用一张表来直观表现出它们的区别。
JVM 内存结构 | Java 内存模型 | |
| 核心关注点 | 数据存储的物理/逻辑分区 | 线程与内存的交互规则 |
| 内容 | 堆、栈、方法区等内存区域 | 主内存、工作内存抽象概念 |
| 目的 | 内存分配与管理 | 多线程内存可见性控制 |
JMM 核心概念
主内存和工作内存
- 主内存:所有线程共享的内存区域,存储所有实例字段、静态字段和数组元素
- 工作内存:每个线程私有的内存空间,存储线程使用变量的副本
当某个线程需要使用到内存中的变量时,他会先从主内存中复制一份该变量的副本到自己的工作内存当中,使用完后再将该变量写入主内存的共享内存中。

内存间的交互操作
- lock/unlock:作用于主内存,标识变量为线程独占状态
- read/load:从主内存读取变量到工作内存
- use/assign:工作内存中使用和赋值操作
- store/write:将工作内存变量写回主内存
内存三大特性
原子性
核心概念:原子性指一个操作或一系列操作要么全部执行成功,要么全部不执行,不会出现执行到一半的状态。
// 原子操作示例
int x = 10; // 原子的:一次性赋值
boolean flag = true; // 原子的
// 非原子操作示例
int i = 0;
i++; // 非原子的,实际包含 3 个步骤:
// 1.读取 i 的值到寄存器
// 2.寄存器值加 1
// 3.写回内存
可见性
核心概念:可见性指当一个线程修改了共享变量的值,其他线程能够立即看到修改后的值。
private boolean running = true; // 主内存中的变量
public void run() {
while (running) { // 工作内存中的副本
// 看不到 running 被改为 false
}
}
public void stop() {
running = false; // 修改主内存,但工作内存可能没更新
}
有序性
核心概念:有序性指程序执行的顺序按照代码的先后顺序执行。但在多线程或优化环境下,指令可能被重排序。
重排序原因:
- 编译器优化重排序
- 处理器指令级并行重排序
- 内存系统重排序
private int x = 0;
private boolean flag = false;
// 线程 1 执行
public void writer() {
x = 42; // 1
flag = true; // 2 可能被重排到 1 之前!
}
// 线程 2 执行
public void reader() {
if (flag) { // 3
System.out.println(x); // 可能输出 0 而不是 42!
}
}
Happens-Before 规则
Happens-Before 规则介绍
Happens-Before是 JMM 的核心概念,它定义了两个操作之间的偏序关系:
- 如果操作 A happens-before 操作 B
- 那么 A 的所有写操作对 B 的读操作都是可见的
有点难看懂,我们用一个简单的例子就能快速理解
// 核心:happens-before ≠ 时间先后
int x = 0;
int y = 0;
// 时间上:先执行 1,后执行 2
x = 1; // 1
y = x + 1; // 2
// 逻辑上:1 happens-before 2
// 所以 2 一定能看到 1 写入的值
A happens-before B 翻译过来就是:A 对 B 可见。
六大 happens-before 规则
- 程序次序规则:**线程内,**按照程序代码顺序,前面的操作 happens-before 后面的操作。
- 监视器锁规则:对一个锁的解锁操作 happens-before 后续对这个锁的加锁操作。
- volatile 变量规则:对一个 volatile 变量的写操作 happens-before 后续对这个变量的读操作。
- 线程启动规则:Thread 对象的start()方法调用 happens-before 该线程的每一个动作。
- 线程终止规则:线程中的所有操作 happens-before 其他线程检测到该线程已经终止
- 传递性规则:如果 A happens-before B,且 B happens-before C,那么 A happens-before C
volatile 关键字
核心概述:volatile是一个重要的关键字,用于告知编译器某个变量的值可能会被程序外部的因素意外修改,从而避免编译器对该变量进行优化。它的主要作用是确保每次访问变量时都从内存中读取最新的值,而不是使用寄存器中的缓存值。
volatile 提供了两大保证:
- 可见性:修改立即对所有线程可见
- 有序性:禁止指令重排序
private volatile boolean flag = false;
private int count = 0;
public void writer() {
count = 42; // 普通写操作
flag = true; // volatile 写操作
}
public void reader() {
if (flag) { // volatile 读操作
// 这里一定能看到 count=42
System.out.println(count);
}
}
JMM 的常见误区
volatile 无法保证原子性
volatile 可以保证可见性和有序性,但和 synchronized 不一样,不能保证原子性
// 错误:以为 volatile 能保证原子性
volatile int count = 0;
count++; // 非原子操作
// 正确:使用原子类或同步
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet();
指令重排序的陷阱
// 可能由于重排序导致问题
int a = 0;
boolean flag = false;
// 线程 1
a = 1; // 1
flag = true; // 2 可能重排到 1 之前
// 线程 2
if (flag) {
System.out.println(a); // 可能输出 0
}
此时我们需对 flag 使用 volatile 关键字修饰即可保证在 a 赋值后再执行 flag=true 操作。


