JMM并非真实存在的物理内存,而是Java虚拟机(JVM)规范中定义的一套抽象内存模型 ,它的核心目标是屏蔽不同硬件和操作系统的内存访问差异,确保Java程序在各种平台上运行时,内存访问的效果保持一致,为并发编程提供统一的规则和保障。简单来说,JMM就是多线程环境下,线程与内存交互的"交通规则"。
一、JMM要解决什么核心问题?
在单线程环境中,我们无需关心内存模型------代码执行顺序、变量读写都是直观且可控的。但多线程环境下,由于CPU缓存、指令重排序等硬件和虚拟机的优化,会出现三个经典问题,这也是JMM诞生的核心原因:
1. 缓存一致性问题
现代CPU为了提升性能,都会在CPU和主内存之间增加高速缓存(L1、L2、L3)。线程在执行时,会先将主内存中的变量读取到自己的工作内存(CPU缓存+线程私有寄存器)中,修改后再刷新回主内存。
当多个线程同时操作同一个共享变量时,就可能出现"缓存不一致":线程A修改了变量的值并存在自己的工作内存中,但还未刷新到主内存;线程B此时从主内存读取该变量的旧值,导致两个线程看到的变量值不一致,最终出现数据错乱。
2. 指令重排序问题
编译器和CPU为了优化执行效率,会在不改变单线程程序语义的前提下,对指令的执行顺序进行重新排列。比如:
// 原代码 int a = 1; int b = 2; // 重排序后可能执行顺序 int b = 2; int a = 1;
单线程下,这种重排序不会影响结果,但多线程下会导致逻辑错误。最典型的就是单例模式的双重检查锁(DCL),若不加volatile,instance = new Singleton()可能被重排序,导致其他线程获取到未初始化的对象。
3. 原子性问题
原子性指的是"一个操作要么全部执行,要么全部不执行,不可被中断"。比如x++看似简单,实则分为"读取x的值、x加1、将结果写回x"三步,这三步在多线程环境下可能被其他线程中断,导致计数不准确。
JMM的核心作用,就是通过一系列规则,解决这三个问题,保证多线程环境下的内存访问安全性。
二、JMM的核心规则:三大特性
JMM通过定义"原子性、可见性、有序性"三大特性,规范线程与内存的交互,这也是我们理解JMM的关键。
1. 原子性(Atomicity)
定义:操作不可被中断,要么全部执行,要么全不执行,不存在中间状态。
JMM对原子性的保证分为两种情况:
-
天然原子操作:JMM默认保证基本类型的读取和赋值操作(除long和double外)是原子的,比如int x = 10、boolean flag = true,这些操作无法被中断。
-
需手动保证的原子操作:对于复合操作(如x++、x += 1),JMM不保证原子性,需通过synchronized、Lock锁或JUC中的原子类(如AtomicInteger)来保证。
java
// 示例:非原子操作与原子操作对比
public class AtomicDemo {
private int count = 0;
private AtomicInteger atomicCount = new AtomicInteger(0);
// 非原子操作:多线程调用会出现计数不准
public void increment() {
count++; // 读取、加1、写回三步,可被中断
}
// 原子操作:通过AtomicInteger保证
public void atomicIncrement() {
atomicCount.incrementAndGet(); // 底层通过CAS实现原子操作
}
}
2. 可见性(Visibility)
定义:当一个线程修改了共享变量的值后,其他线程能立即看到该变量的最新值。
如前文所述,由于工作内存的存在,线程修改共享变量后,若未及时刷新到主内存,其他线程读取的就是旧值,这就是可见性问题。JMM提供了三种方式保证可见性:
-
volatile关键字:强制线程每次读取变量时,都从主内存获取,而非工作内存;修改变量后,立即刷新到主内存,确保其他线程能及时看到最新值。
-
synchronized/Lock:线程进入同步块(或获取锁)时,会将工作内存中的共享变量清空,重新从主内存读取;退出同步块(或释放锁)时,会将工作内存中修改后的变量刷新到主内存。
-
final关键字:final修饰的变量初始化后不可修改,JMM保证其初始化完成后,对所有线程可见。
java
// 示例:volatile保证可见性
public class VisibilityDemo {
// 不加volatile,线程B可能陷入无限循环
private volatile boolean running = true;
public void stop() {
running = false; // 修改后立即刷新到主内存
}
public void work() {
// 每次循环都从主内存读取running的值
while (running) {
// 执行任务
}
System.out.println("线程停止");
}
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo = new VisibilityDemo();
new Thread(demo::work).start();
Thread.sleep(1000);
demo.stop(); // 线程A修改running,线程B立即看到
}
}
3. 有序性(Ordering)
定义:程序执行顺序与代码编写顺序一致,避免因指令重排序导致的逻辑错误。
JMM允许编译器和CPU进行指令重排序,但会通过以下方式保证有序性:
-
volatile关键字:禁止指令重排序,确保volatile变量前后的操作不会被重排,保证执行顺序。
-
synchronized/Lock:同步块内的指令会被当作一个整体执行,禁止重排序。
-
happens-before原则:JMM定义的一套"先行发生"规则,无需任何同步手段,就能保证某些操作的有序性(下文详细说明)。
最经典的案例就是单例模式的双重检查锁,必须给instance加volatile,禁止instance = new Singleton()的指令重排:
// 正确的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) { // 第二次检查
// 若不加volatile,可能重排为:分配内存→引用指向内存→初始化对象
// 导致其他线程获取到未初始化的instance
instance = new Singleton();
}
}
}
return instance;
}
}
三、JMM的核心机制:内存屏障与happens-before原则
前面提到的volatile、synchronized的作用,底层都依赖JMM的核心机制------内存屏障;而happens-before原则则是JMM判断有序性的核心依据。
1. 内存屏障(Memory Barrier)
内存屏障是插入在指令序列中的特殊指令,用于禁止指令重排序,并强制刷新缓存,保证可见性。JMM将内存屏障分为4类,对应不同的作用:
-
LoadLoad屏障:确保屏障前的读取操作完成后,再执行屏障后的读取操作。
-
StoreStore屏障:确保屏障前的写入操作刷新到主内存后,再执行屏障后的写入操作。
-
LoadStore屏障:确保屏障前的读取操作完成后,再执行屏障后的写入操作。
-
StoreLoad屏障:确保屏障前的写入操作刷新到主内存后,再执行屏障后的读取操作(最强大,volatile的底层核心)。
JVM会根据不同的关键字,自动插入对应的内存屏障,开发者无需手动操作。比如volatile变量的写操作后,会插入StoreStore和StoreLoad屏障;读操作前,会插入LoadLoad和LoadStore屏障,从而禁止重排序并保证可见性。
2. happens-before原则
happens-before(先行发生)是JMM定义的一套规则,用于判断两个操作之间的有序性和可见性。如果操作A happens-before 操作B,那么A的执行结果对B可见,且A的执行顺序在B之前。
常用的happens-before规则(无需记全,掌握核心即可):
-
程序次序规则:单线程中,代码编写顺序在前的操作,happens-before于编写顺序在后的操作(单线程天然有序)。
-
volatile规则:对volatile变量的写操作,happens-before于后续对该变量的读操作。
-
锁规则:释放锁的操作,happens-before于后续获取同一把锁的操作。
-
线程启动规则:Thread.start()操作,happens-before于线程内的所有操作。
举个例子:线程A调用demo.stop()(写volatile变量running),线程B执行demo.work()(读volatile变量running),根据volatile规则,A的写操作happens-before于B的读操作,因此A修改的running值,B能立即看到。
四、JMM常见关键字实践总结
我们日常开发中,最常接触的就是volatile和synchronized,二者在JMM中的作用的区别,是面试和开发的重点,用表格清晰总结:
| 关键字 | 原子性 | 可见性 | 有序性 | 适用场景 |
|---|---|---|---|---|
| volatile | 不保证(仅保证单次读写原子) | 保证 | 保证(禁止重排序) | 状态标记(如running)、DCL单例 |
| synchronized | 保证(同步块内操作) | 保证 | 保证(同步块内有序) | 复杂临界区(如多线程修改共享变量) |
补充:final关键字虽然不涉及同步,但JMM保证其可见性和不可变性,适合修饰常量或不可变对象。