一、为什么需要内存模型(JMM)?
在多核CPU时代,并发编程是提升程序性能的核心手段,但是并发带来了三大经典问题:
可见性问题:线程A修改的变量,线程B无法立即看到
原子性问题:i++操作在机器指令层面可能被中断
有序性问题:代码执行顺序与编写顺序不一致
这些问题的根源在于:
-
CPU多级缓存架构导致内存可见性问题
-
编译器/处理器优化引发指令重排序
-
线程切换带来的原子性破坏

二、JMM架构设计
2.1 内存抽象模型
JMM定义了两个核心概念:
-
主内存(Main Memory):所有线程共享的内存区域
-
工作内存(Working Memory):每个线程私有的内存副本
从上图来看,线程 1 与线程 2 之间如果要进行通信的话,必须要经历下面 2 个步骤:
- 线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。
- 线程 2 到主存中读取对应的共享变量的值。
也就是说,JMM 为共享变量提供了可见性的保障。
不过,多线程下,对主内存中的一个共享变量进行操作有可能诱发线程安全问题。举个例子:
- 线程 1 和线程 2 分别对同一个共享变量进行操作,一个执行修改,一个执行读取。
- 线程 2 读取到的是线程 1 修改之前的值还是修改后的值并不确定,都有可能,因为线程 1 和线程 2 都是先将共享变量从主内存拷贝到对应线程的工作内存中。
2.2 内存交互协议
JMM定义了8种原子操作保证内存可见性:
操作 | 作用范围 | 说明 |
---|---|---|
lock | 主内存变量 | 标记变量为线程独占状态 |
unlock | 主内存变量 | 释放变量的锁定状态 |
read | 主内存变量 | 将变量值传输到线程工作区 |
load | 工作内存 | 将read的值放入变量副本 |
use | 工作内存 | 将变量值传递给执行引擎 |
assign | 工作内存 | 将执行结果赋值给变量副本 |
store | 工作内存 | 将变量值传送到主内存 |
write | 主内存变量 | 将store的值放入主内存变量 |
三、happens-before原则
3.1 核心规则
程序顺序规则:单线程内的操作按代码顺序保证有序性
volatile规则:volatile写操作先于后续的读操作
锁规则:解锁操作先于后续的加锁操作
传递性规则:A→B且B→C,则A→C
线程启动规则:Thread.start()先于线程内所有操作
下面这张是 《Java 并发编程的艺术》这本书中的一张 JMM 设计思想的示意图:
3.2 实际案例
// 示例:双重检查锁定单例模式
public class Singleton {private volatile static Singleton instance;public static Singleton getInstance() {if (instance == null) { // 第一次检查synchronized (Singleton.class) { // 加锁if (instance == null) { // 第二次检查instance = new Singleton(); // volatile写}}}return instance;}
}
这里volatile的happens-before关系保证了:
对象初始化完成 → 写操作
写操作 → 读操作
四、内存屏障与指令重排序
4.1 屏障类型
屏障类型 | 作用 |
---|---|
LoadLoad屏障 | 禁止读操作重排序 |
StoreStore屏障 | 禁止写操作重排序 |
LoadStore屏障 | 禁止读后写重排序 |
StoreLoad屏障 | 禁止写后读重排序(全能屏障) |
4.2 volatile实现原理
public class VolatileExample {private volatile int flag = 0;public void writer() {flag = 1; // StoreStore屏障 + StoreLoad屏障}public void reader() {if (flag == 1) { // LoadLoad屏障 + LoadStore屏障// do something}}
}
volatile变量的读写会插入内存屏障:
写操作前插入StoreStore屏障
写操作后插入StoreLoad屏障
读操作前插入LoadLoad屏障
读操作后插入LoadStore屏障
五、JMM与JVM内存结构对比
特性 | JMM | JVM内存结构 |
---|---|---|
关注点 | 多线程内存可见性问题 | 内存区域划分与管理 |
核心概念 | 主内存、工作内存 | 堆、栈、方法区等 |
规范级别 | 语言级内存模型 | 虚拟机实现规范 |
可见性保证 | 通过happens-before规则 | 不直接处理可见性问题 |
典型应用 | volatile、synchronized语义 | 对象分配、垃圾回收 |
六、并发问题解决方案
6.1 可见性问题
可见性问题概述:在多线程环境中,每个线程都有自己的工作内存,线程对变量的操作是先从主内存拷贝到工作内存,操作完成后再写回主内存。这就可能导致一个线程对变量的修改,其他线程不能及时看到,从而引发可见性问题。
-
volatile关键字:强制所有读写直接操作主内存
public class VolatileVisibilityExample {private volatile boolean flag = false;public void writer() {flag = true; // 写操作直接更新主内存}public void reader() {while (!flag) {// 等待 flag 变为 true}// 由于 flag 是 volatile 变量,能及时看到 writer 线程对 flag 的修改System.out.println("Flag is now true");}
}
-
synchronized同步块:解锁时自动刷新工作内存到主内存
public class SynchronizedVisibilityExample {private boolean flag = false;private final Object lock = new Object();public void writer() {synchronized (lock) {flag = true; // 修改共享变量} // 退出同步块,将修改刷新到主内存}public void reader() {synchronized (lock) {// 进入同步块,从主内存读取最新的 flag 值if (flag) {System.out.println("Flag is true");}}}
}
-
final关键字:正确发布的不可变对象保证可见性
public class FinalVisibilityExample {private final int value;public FinalVisibilityExample(int value) {this.value = value; // 初始化 final 变量}public int getValue() {return value;}
}
6.2 原子性问题
原子性问题概述:原子性是指一个操作或一系列操作要么全部执行,要么全部不执行,不会被其他线程中断。在多线程环境中,如果多个线程同时对一个共享变量进行读写操作,可能会导致数据不一致的问题,因为这些操作可能不是原子性的。
-
Atomic原子类:基于CAS实现无锁编程
import java.util.concurrent.atomic.AtomicInteger;public class AtomicExample {private AtomicInteger counter = new AtomicInteger(0);public void increment() {counter.incrementAndGet(); // 原子性自增操作}public int getCounter() {return counter.get();}
}
-
synchronized同步:通过互斥保证原子性
public class SynchronizedAtomicExample {private int counter = 0;public synchronized void increment() {counter++; // 同步方法,保证原子性}public synchronized int getCounter() {return counter;}
}
-
Lock接口实现:显式锁控制临界区
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class LockAtomicExample {private int counter = 0;private final Lock lock = new ReentrantLock();public void increment() {lock.lock();try {counter++; // 临界区,保证原子性} finally {lock.unlock();}}public int getCounter() {lock.lock();try {return counter;} finally {lock.unlock();}}
}
6.3 有序性问题
有序性问题概述:在多线程环境中,编译器和处理器为了提高性能,可能会对指令进行重排序。重排序可能会导致程序的执行顺序与代码的编写顺序不一致,从而引发有序性问题。
-
volatile:禁止指令重排序
public class VolatileOrderingExample {private int a = 0;private volatile boolean flag = false;public void writer() {a = 1; // 操作 1flag = true; // 操作 2,由于 flag 是 volatile 变量,操作 1 不会重排序到操作 2 之后}public void reader() {if (flag) { // 操作 3int i = a; // 操作 4,操作 4 不会重排序到操作 3 之前}}
}
-
synchronized:保证临界区内代码串行执行
public class SynchronizedOrderingExample {private int a = 0;private int b = 0;private final Object lock = new Object();public void writer() {synchronized (lock) {a = 1; // 操作 1b = 2; // 操作 2,操作 1 和操作 2 会按顺序执行}}public void reader() {synchronized (lock) {int x = b; // 操作 3int y = a; // 操作 4,操作 3 和操作 4 会按顺序执行}}
}
-
final:正确构造的对象保证初始化安全
public class FinalOrderingExample {private final int value;public FinalOrderingExample(int value) {this.value = value; // 正确初始化 final 字段}public int getValue() {return value;}
}
七、实战:诊断内存可见性问题
public class VisibilityDemo {boolean ready = false;int result = 0;int number = 1;public void write() {number = 2; // 操作1ready = true; // 操作2}public void read() {if (ready) { // 操作3result = number; // 操作4}}
}
可能出现的结果:
线程A执行write()
线程B执行read()可能得到result=1(指令重排序导致操作2先于操作1)
解决方案:
将ready声明为volatile
使用synchronized同步方法