JavaGuide内存模型:JMM与volatile关键字解析
为什么需要JMM?
你是否曾遇到过这样的情况:多线程程序在本地测试一切正常,上线后却出现诡异的变量值异常?这很可能是Java内存模型(JMM)在背后"作祟"。JMM(Java Memory Model)是Java并发编程的基础,它定义了线程和主内存之间的抽象关系,解决了多线程环境下共享变量的可见性、原子性和有序性问题。
读完本文你将掌握:
- CPU缓存模型如何导致内存可见性问题
- 指令重排序对多线程程序的影响
- JMM如何通过happens-before原则保证内存可见性
- volatile关键字的实现原理与正确使用场景
- 如何用Atomic原子类构建线程安全的计数器
CPU缓存与内存可见性问题
现代计算机为了解决CPU处理速度与内存访问速度的差异,引入了多级缓存结构。CPU Cache缓存内存数据,极大提升了性能,但也带来了缓存一致性问题。

当多个CPU核心同时操作同一变量时,如果各自缓存中的值不一致,就会导致数据混乱。例如两个线程同时执行i++操作:
- 线程A从主内存读取i=1到CPU缓存
- 线程B也从主内存读取i=1到CPU缓存
- 两个线程各自执行+1操作,缓存中i都变为2
- 最终写回主内存后i=2,而非期望的3
CPU通过缓存一致性协议(如MESI协议)解决此问题,确保多个缓存中的数据保持一致。

指令重排序的"陷阱"
为提升性能,编译器和CPU会对指令进行重排序。例如这段代码:
int a = 1; // 操作1
int b = 2; // 操作2
int c = a + b; // 操作3
编译器可能会将操作1和2重排序,因为它们之间没有数据依赖。这种优化在单线程下没问题,但在多线程环境可能导致意外结果。
指令重排序主要有三种类型:
- 编译器优化重排:JVM编译器在不改变单线程语义的前提下调整指令顺序
- 指令并行重排:CPU将多条无依赖的指令并行执行
- 内存系统重排:CPU缓存导致的"看起来像"重排序的现象
JMM通过内存屏障(Memory Barrier)禁止特定类型的重排序,确保多线程环境下的执行顺序。
JMM的核心原理
JMM抽象了线程和主内存之间的关系,定义了主内存(所有线程共享的内存区域)和本地内存(每个线程私有的内存区域)。线程操作共享变量时,必须通过主内存进行交互。

线程间通信需经过两个步骤:
- 线程A将本地内存中修改的共享变量刷新到主内存
- 线程B从主内存读取最新的共享变量值到本地内存
JMM通过happens-before原则建立操作之间的偏序关系,确保一个操作的结果对另一个操作可见:
- 程序顺序规则:同一线程中,前面的操作happens-before后面的操作
- volatile规则:对volatile变量的写操作happens-before后续的读操作
- 锁定规则:解锁操作happens-before后续的加锁操作
- 传递性:若A happens-before B且B happens-before C,则A happens-before C

volatile关键字解析
volatile是JMM提供的轻量级同步机制,它能保证:
1. 可见性保证
当一个线程修改volatile变量后,其他线程能立即看到最新值。这是因为:
- 写volatile变量时,JMM会把线程本地内存中的变量刷新到主内存
- 读volatile变量时,JMM会使线程本地内存中的变量失效,强制从主内存读取
2. 禁止指令重排序
volatile通过内存屏障实现指令重排禁止:
- 写操作前插入StoreStore屏障,防止前面的写操作被重排到volatile写之后
- 写操作后插入StoreLoad屏障,防止后面的读操作被重排到volatile写之前
- 读操作前插入LoadLoad屏障,防止后面的普通读被重排到volatile读之前
- 读操作后插入LoadStore屏障,防止后面的写操作被重排到volatile读之前
3. 不保证原子性
volatile不能保证复合操作的原子性。例如count++包含三个操作:
- 读取count的值
- 对count加1
- 将结果写回count
要实现原子操作,可以使用Atomic原子类:
// 使用AtomicInteger实现线程安全的计数器
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet(); // 原子自增操作
volatile的正确使用场景
1. 状态标志
private volatile boolean isRunning = true;
public void stop() {
isRunning = false; // 线程间可见的状态变更
}
public void run() {
while (isRunning) {
// 执行任务
}
}
2. 双重检查锁定(DCL)
在单例模式中使用volatile防止指令重排序:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 禁止重排序
}
}
}
return instance;
}
}
3. 独立观察
定期"发布"观察结果供其他线程使用:
private volatile int temperature;
// 传感器线程定期更新温度
new Thread(() -> {
while (true) {
temperature = readSensor(); // 写volatile变量
Thread.sleep(1000);
}
}).start();
// 显示线程读取温度
new Thread(() -> {
while (true) {
display(temperature); // 读volatile变量
Thread.sleep(500);
}
}).start();
JMM与并发编程三特性
JMM通过各种机制保证并发编程的三大特性:
可见性
- 实现方式:volatile、synchronized、Lock
- 原理:修改后的变量立即写回主内存,读取时从主内存加载最新值
原子性
- 实现方式:synchronized、Lock、Atomic原子类
- 原理:保证操作不可分割,不会被其他线程中断
有序性
- 实现方式:volatile、synchronized、Lock、happens-before原则
- 原理:通过内存屏障禁止指令重排序,建立操作间的顺序关系
总结
Java内存模型是并发编程的基础,它通过抽象线程与主内存的关系,定义了共享变量的访问规则。volatile关键字是JMM的重要实现,提供了轻量级的线程安全保障,但不能替代锁或原子类。
理解JMM关键在于掌握:
- 可见性:volatile如何保证变量修改对其他线程可见
- 有序性:happens-before原则如何建立操作间的顺序关系
- 原子性:何时需要使用Atomic原子类或锁机制
正确运用JMM知识,可以避免90%以上的并发编程问题。建议深入阅读JMM详解和Atomic原子类总结获取更多实践技巧。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



