什么是JMM?
JMM的由来
一般来说,编程语言也可以直接复用操作系统层面的内存模型。不过,不同的操作系统内存模型不同。如果直接复用操作系统层面的内存模型,就可能会导致同样一套代码换了一个操作系统就无法执行了。但Java 语言是跨平台的,它需要自己提供一套内存模型以屏蔽系统差异。
这只是 JMM 存在的其中一个原因。实际上,对于 Java 来说,你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范(JSR133),除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
Java线程之间的通信由Java内存模型(简称JMM)控制,从抽象的角度来说,JMM定义了线程和主内存之间的抽象关系。JMM的抽象示意图如图所示:

什么是主内存?什么是本地内存?
- 主内存:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量,还是局部变量,类信息、常量、静态变量都是放在主内存中。为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
- 本地内存:每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。每个线程只能操作自己本地内存中的变量,无法直接访问其他线程的本地内存。如果线程间需要通信,必须通过主内存来进行。本地内存是 JMM 抽象出来的一个概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
从图中可以看出:
- 所有的共享变量都存在主内存中。
- 每个线程都保存了一份该线程使用到的共享变量的副本。
如果线程A与线程B之间要通信的话,必须经历下面2个步骤:
- 线程A将本地内存A中更新过的共享变量刷新到主内存中去。
- 线程B到主内存中去读取线程A之前已经更新过的共享变量。
所以,线程A无法直接访问线程B的工作内存,线程间通信必须经过主内存。
所以线程B并不是直接去主内存中读取共享变量的值,而是先在本地内存B中找到这个共享变量,发现这个共享变量已经被更新了,然后本地内存B去主内存中读取这个共享变量的新值,并拷贝到本地内存B中,最后线程B再读取本地内存B中的新值。
那么怎么知道这个共享变量的被其他线程更新了呢?这就是JMM的功劳了,也是JMM存在的必要性之一。JMM通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性保证。
Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则。
关于JMM
为什么这么设计?
- 减少CPU等待时间
- 空间局部性原理、时间局部性原理
别的编程语言也有内存模型吗?
只要支持并发的现代编程语言都有类似的概念,但是不一定是共享内存模型。
按需禁用缓存和编译优化
happen before
Happens-Before 的含义:前面一个操作的结果对后续操作是可见的。
happens-before的内容:
- 程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- 管程中锁的规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- start规则:如果线程A执行操作ThreadB.start()启动线程B,那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作
- join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
Happens-Before 的作用:用于程序员判断程序是否有线程安全问题,另外Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。
Happens-Before 的语义是一种因果关系。在现实世界里,如果 A 事件是导致 B 事件的起因,那么 A 事件一定是先于(Happens-Before)B 事件发生的,这个就是 Happens-Before 语义的现实理解。
在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。
1. 程序的顺序性规则
这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。
这还是比较容易理解的,在如下代码中:
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里x会是多少呢?
}
}
}
按照程序的顺序,第 6 行代码 “x = 42;” Happens-Before 于第 7 行代码 “v = true;”,这就是规则 1 的内容,也比较符合单线程里面的思维:程序前面对某个变量的修改一定是对后续操作可见的。
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里x会是多少呢?
}
}
}
2. volatile 变量规则
这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。
这个就有点费解了,对一个 volatile 变量的写操作相对于后续对这个 volatile 变量的读操作可见,这怎么看都是禁用缓存的意思啊,貌似和 1.5 版本以前的语义没有变化啊?如果单看这个规则,的确是这样,但是如果我们关联一下规则 3,就有点不一样的感觉了。
3. 传递性
这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
我们将规则 3 的传递性应用到我们的例子中,会发生什么呢?可以看下面这幅图:

从图中,我们可以看到:
“x=42” Happens-Before 写变量 “v=true” ,这是规则 1 的内容;
写变量“v=true” Happens-Before 读变量 “v=true”,这是规则 2 的内容 。
再根据这个传递性规则,我们得到结果:“x=42” Happens-Before 读变量“v=true”。这意味着什么呢?
如果线程 B 读到了“v=true”,那么线程 A 设置的“x=42”对线程 B 是可见的。也就是说,线程 B 能看到 “x == 42” 。
代码如下:
public class Transitivity {
int x = 0;
volatile boolean v = false;
public static void main(String[] args) throws InterruptedException {
Transitivity transitivity = new Transitivity();
Thread threadA = new Thread(() -> transitivity.writer());
Thread threadB = new Thread(() -> transitivity.reader());
threadA.start();
threadA.join();
threadB.start();
threadB.join();
}
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里x会是42
System.out.println(x);
}
}
}
4. 管程中锁的规则
这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
要理解这个规则,就首先要了解“管程指的是什么”。管程是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。
管程中的锁在 Java 里是隐式实现的,例如下面的代码,在进入同步块之前,会自动加锁,而在代码块执行完会自动释放锁,加锁以及释放锁都是编译器帮我们实现的。
synchronized (this) { //此处自动加锁
// x是共享变量,初始值=10
if (this.x < 12) {
this.x = 12;
}
} //此处自动解锁
所以结合规则 4——管程中锁的规则,可以这样理解:假设 x 的初始值是 10,线程 A 执行完代码块后 x 的值会变成 12(执行完自动释放锁),线程 B 进入代码块时,能够看到线程 A 对 x 的写操作,也就是线程 B 能够看到 x==12。这个也是符合我们直觉的,应该不难理解。
5. 线程 start() 规则
这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
换句话说就是,如果线程 A 调用线程 B 的 start() 方法(即在线程 A 中启动线程 B),那么该 start() 操作 Happens-Before 于线程 B 中的任意操作。具体可参考下面示例代码。
public class StartHappenBefore {
public static void main(String[] args) {
int i = 0;
Thread B = new Thread(() -> {
// 主线程调用B.start()之前
// 所有对共享变量的修改,此处皆可见
// 此例中,i==1
System.out.println(i);
});
ThreadUtil.sleep(500);
// 此处对共享变量i修改
i = 1;
// 主线程启动子线程
B.start();
}
}
6. 线程 join() 规则
这条是关于线程等待的。它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。
换句话说就是,如果在线程 A 中,调用线程 B 的 join() 并成功返回,那么线程 B 中的任意操作 Happens-Before 于该 join() 操作的返回。具体可参考下面示例代码。
public class JoinHappenBefore {
public static void main(String[] args) {
int i = 0;
Thread B = new Thread(() -> {
// 此处对共享变量i修改
i = 1;
});
// 主线程启动子线程
B.start();
B.join();
// 子线程所有对共享变量的修改
// 在主线程调用B.join()之后皆可见
// 此例中,i==1
System.out.println(i);
}
}
volatile关键字
在Java中,volatile关键字有特殊的内存语义。volatile主要有以下两个功能:
- 保证变量的内存可见性
- 禁止volatile变量与普通变量重排序(JSR133提出,Java 5 开始才有这个“增强的volatile内存语义”)
内存可见性
public class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // step 1
flag = true; // step 2
}
public void reader() {
if (flag) { // step 3
System.out.println(a); // step 4
}
}
}
在这段代码里,我们使用volatile
关键字修饰了一个boolean
类型的变量flag
。
所谓内存可见性,指的是当一个线程对volatile
修饰的变量进行写操作(比如step 2)时,JMM会立即把该线程对应的本地内存中的共享变量的值刷新到主内存;当一个线程对volatile
修饰的变量进行读操作(比如step 3)时,JMM会把立即该线程对应的本地内存置为无效,从主内存中读取共享变量的值。
禁止重排序
在JSR-133之前的旧的Java内存模型中,是允许volatile变量与普通变量重排序的。那上面的案例中,可能就会被重排序成下列时序来执行:
- 线程A写volatile变量,step 2,设置flag为true;
- 线程B读同一个volatile,step 3,读取到flag为true;
- 线程B读普通变量,step 4,读取到 a = 0;
- 线程A修改普通变量,step 1,设置 a = 1;
可见,如果volatile变量与普通变量发生了重排序,虽然volatile变量能保证内存可见性,也可能导致普通变量读取错误。
所以在旧的内存模型中,volatile的写-读就不能与锁的释放-获取具有相同的内存语义了。为了提供一种比锁更轻量级的线程间的通信机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序。
编译器还好说,JVM是怎么还能限制处理器的重排序的呢?它是通过内存屏障来实现的。
什么是内存屏障?硬件层面,内存屏障分两种:读屏障(Load Barrier)和写屏障(Store Barrier)。内存屏障有两个作用:
- 阻止屏障两侧的指令重排序;
- 强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效。
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。编译器选择了一个比较保守的JMM内存屏障插入策略,这样可以保证在任何处理器平台,任何程序中都能得到正确的volatile内存语义。这个策略是:
- 在每个volatile写操作前插入一个StoreStore屏障;
- 在每个volatile写操作后插入一个StoreLoad屏障;
- 在每个volatile读操作后插入一个LoadLoad屏障;
- 在每个volatile读操作后再插入一个LoadStore屏障。
源码验证:https://cr.openjdk.org/~coleenp/8139203.01/src/share/vm/interpreter/bytecodeInterpreter.cpp-.html
再介绍一下volatile与普通变量的重排序规则:
- 如果第一个操作是volatile读,那无论第二个操作是什么,都不能重排序;
- 如果第二个操作是volatile写,那无论第一个操作是什么,都不能重排序;
- 如果第一个操作是volatile写,第二个操作是volatile读,那不能重排序。
举个例子,我们在案例中step 1,是普通变量的写,step 2是volatile变量的写,那符合第2个规则,这两个steps不能重排序。而step 3是volatile变量读,step 4是普通变量读,符合第1个规则,同样不能重排序。
但如果是下列情况:第一个操作是普通变量读,第二个操作是volatile变量读,那是可以重排序的:
// 声明变量
int a = 0; // 声明普通变量
volatile boolean flag = false; // 声明volatile变量
// 以下两个变量的读操作是可以重排序的
int i = a; // 普通变量读
boolean j = flag; // volatile变量读
原子性
对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性(基于这点,我们通过会认为volatile不具备原子性)。volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。
64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性。
DCL
public class SingleDcl {
// volatile 防止指令重排序
private volatile static SingleDcl singleDcl = null;
private static SingleDcl getInstance() {
if (singleDcl == null) {
synchronized (SingleDcl.class) {
if (singleDcl == null) {
// 1.开辟内存空间
// 2.对象初始化
// 3.singleDcl指向内存空间的地址
singleDcl = new SingleDcl();
}
}
}
return singleDcl;
}
}
图示如下:

final关键字
final 修饰变量时,初衷是告诉编译器:这个变量生而不变。
final 字段的可见性由 JMM 的初始化安全性保证,效果类似于内存屏障,但实现上依赖编译器约束和硬件内存模型。在 x86 上,通常不需要显式屏障;在 ARM 等弱内存模型架构上,可能插入 StoreStore 屏障。