java内存模型JMM
Java 内存模型(JMM)
Java 内存模型(JMM)是一套规范,用来屏蔽不同硬件和操作系统的内存访问差异,让 Java 程序在各种平台下都能实现一致的并发效果。它的核心目标是解决多线程下的原子性、可见性、有序性问题,定义了线程和主内存之间的抽象关系。
一、JMM 核心:三大特性
JMM 围绕原子性、可见性、有序性三大特性设计,这是多线程并发安全的基础。
1. 原子性(Atomicity)
定义:一个操作是不可中断的,要么全部执行成功,要么全部不执行,执行过程中不会被其他线程打断。
- 基本数据类型的赋值 / 读取是原子操作(如
int a=1;、b=a;)。 - 复合操作不是原子性(如
i++,本质是「读取 - 修改 - 写入」三步)。
保证原子性的方式:
synchronized(互斥锁,强制操作串行执行)。java.util.concurrent.atomic包下的原子类(CAS 无锁算法)。
2. 可见性(Visibility)
定义:当一个线程修改了共享变量的值,其他线程能立即感知到这个修改。
- JMM 规定:所有共享变量存在主内存,每个线程有自己的工作内存(线程私有)。
- 线程操作变量:先从主内存拷贝到工作内存 → 修改 → 写回主内存。
- 可见性问题:线程 A 修改了变量,还没写回主内存,线程 B 读取的是旧值。
保证可见性的方式:
volatile(轻量级,强制直接读写主内存)。synchronized(解锁前必须将变量刷回主内存)。final(修饰的变量初始化后不可修改,天然可见)。
3. 有序性(Ordering)
定义:程序执行的顺序按照代码的先后顺序执行。
- 单线程:天然有序(as-if-serial 语义,不管怎么重排,结果不变)。
- 多线程:指令重排会破坏有序性,导致线程间执行顺序混乱。
保证有序性的方式:
volatile(禁止指令重排)。synchronized(同一时刻只有一个线程执行,无重排问题)。Happens-Before原则(JMM 定义的有序性规则,无需手动加锁)。
二、volatile 原理与使用场景
volatile 是 JMM 提供的轻量级同步机制,只保证可见性 + 有序性,不保证原子性。
1. 核心原理
可见性实现:
被
volatile修饰的变量,线程修改后立即刷回主内存,其他线程读取时直接从主内存加载,跳过工作内存。有序性实现:
插入内存屏障,禁止编译器和 CPU 进行指令重排。
2. 关键特性
- 不保证原子性:
volatile int i; i++依然线程不安全。 - 轻量级:无锁,不会阻塞线程(比
synchronized高效)。
3. 最佳使用场景
状态标志位(单一写、多读)
1
2
3
4
5
6
7
8
9
10
11volatile boolean flag = false;
// 线程A 修改标志
public void stop() {
flag = true;
}
// 线程B 感知标志
public void run() {
while (!flag) {
// 执行业务
}
}单例模式双重校验锁(DCL)
必须用
volatile禁止指令重排,防止对象半初始化。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class Singleton {
// 关键点:必须加 volatile
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 第一次判断:避免每次都加锁,提升效率
if (instance == null) {
synchronized (Singleton.class) {
// 第二次判断:防止多线程同时进入第一层if
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}instance = new Singleton() 这一行代码,不是原子的,CPU 会拆成 3 步:
分配内存空间
在堆上开辟一块内存,此时对象是默认零值(未初始化)。
初始化对象
调用构造方法,给成员变量赋值,对象真正 “建好”。
将 instance 引用指向这块内存
instance = 内存地址
如果不加
volatile关键字,多线程下:线程 A 进入 synchronized 里:
执行
new Singleton()走到步骤 3:
instance = 地址→ 此时 instance 已经!= null
但还没执行步骤 2:对象构造方法还没跑,字段都是默认值
这时候线程 B 来了:
1
if (instance == null)判断结果是 false
→ 直接 return instance
→ 线程 B 拿到了一个存在、但没初始化完的对象
后果:
线程 B 去使用这个对象
→ 访问成员变量全是默认值(0 /null/false)
→ 空指针、状态错乱、程序诡异崩溃
加 volatile 后
volatile禁止指令重排保证顺序永远是:
- 分配内存
- 初始化对象
- 引用赋值
线程 B 只有两种可能:
- 要么看到
instance == null - 要么看到 完全初始化好的对象
绝对看不到半初始化的中间状态。
三、指令重排与内存屏障
1. 指令重排
定义:编译器 / CPU 为了优化性能,在不改变单线程执行结果的前提下,调整代码指令的执行顺序。
- 单线程:安全,无影响。
- 多线程:不安全,会导致线程间数据错乱。
重排类型:
- 编译器重排(编译阶段)。
- 处理器重排(运行阶段)。
2. 内存屏障(Memory Barrier)
JMM 通过内存屏障解决指令重排和可见性问题,是 CPU 层面的指令。
四大屏障作用:
- LoadLoad:禁止读操作重排。
- StoreStore:禁止写操作重排。
- LoadStore:禁止读→写重排。
- StoreLoad:禁止写→读重排(全能屏障,开销最大)。
volatile 的屏障策略:
写操作前 / 后插入屏障。
读操作后插入屏障。
→ 最终效果:禁止 volatile 变量前后的指令重排。
四、堆、栈、方法区与线程的关系
这是Java 内存区域(运行时数据区),和 JMM 是不同维度的概念,但和线程强相关:
| 内存区域 | 存储内容 | 线程归属 | 核心特点 |
|---|---|---|---|
| 堆(Heap) | 对象实例、数组 | 线程共享 | GC 主要区域,OOM 高发区 |
| 虚拟机栈 | 局部变量表、方法栈帧 | 线程私有 | 生命周期与线程一致 |
| 本地方法栈 | native 方法执行 | 线程私有 | 与虚拟机栈类似 |
| 程序计数器 | 当前线程执行的字节码行号 | 线程私有 | 唯一不会 OOM 的区域 |
| 方法区 | 类信息、常量、静态变量、即时编译代码 | 线程共享 | JDK8 后叫元空间(Metaspace) |
核心关系总结
- 线程私有区域:虚拟机栈、本地方法栈、程序计数器
- 每个线程独立一份,无线程安全问题(不需要 JMM 保证)。
- 方法执行时创建栈帧,方法结束自动释放内存。
- 线程共享区域:堆、方法区
- 所有线程共用一块内存,存在线程安全问题(需要 JMM 保证三大特性)。
- 共享变量(实例变量、静态变量)都存在这里,是并发问题的根源。