为什么要有内存模型?
在介绍 Java 内存模型之前应该首先了解一下计算机的内存模型。
在计算机执行程序时,每条指令都在 CPU 中执行,而执行过程中,必然会对计算机内存进行数据的读取和输入,然而 CPU 的执行速度非常快,大大的超过了从内存中读取和写入数据的速度,因此就有了高速缓存。
有了高速缓存后,程序的执行过程变为:
- 开始时,从内存中读取数据拷贝到高速缓存
- CPU 直接从高速缓存中读取和写入数据
- 结束后,将高速缓存中数据写入内存中
分别对单线程、单核 CPU 中多线程、多核 CPU 中多线程分析:
- 单线程:缓存被线程独占,不会出现缓存冲突。
- 单核 CPU 中多线程:不同线程访问进程中的共享数据时,CPU 会将共享变量复制到缓存中,不同线程通过访问同一进程的虚拟地址,虚拟地址经过 MMU 再映射成物理地址,最终 CPU 通过物理地址会访问到同一块缓存区域,而且同一时刻只有一个线程在执行,因此不会出现缓存冲突。
- 多核 CPU 中多线程:在多核 CPU 中,每个 CPU 都有缓存,并且它们是相互独立的,这样在不同 CPU 中的线程执行时,可能会导致各自缓存中的数据不一致。
在多核 CPU 多线程场景下就可能出现缓存一致性问题。这是由于每个核都有自己独立的缓存导致数据不同步导致的。
什么是 JMM ?
Java 内存模型(JMM) 不像 JVM 内存结构一样是真实存在的,JMM 只是一个抽象的概念。
JMM 决定了一个线程对共享变量的写入何时对另一线程可见。
JMM 屏蔽了各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的访问效果。
JMM 描述了 Java 程序中各种变量(线程共享变量)的访问规则,以及在 JVM 中将变量存储到内存和从内存中读取变量这样的细节。
JMM 是语言级别的内存模型。
JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等问题。
主内存和工作内存
JMM 规定所有变量都储存在主内存中,每条线程都有字节的工作内存,线程的工作内存中保存了被该线程使用到的主内存中变量的副本拷贝。
线程对变量的所有操作(读/写)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程的工作内存之间互相隔离,线程之间的变量传递通过主内存来实现。
从变量、主内存、工作内存的定义来看,主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应虚拟机栈中的部分区域。
原子性、可见性、有序性
JMM 是一种规则,它决定了一个线程对共享变量的写入何时对另一个线程可见。JMM 是围绕着在并发过程中如何处理原子性、可见性和有序性这 3 个特征来建立的。
原子性(Atomicity)
大致可以认为基本数据类型的访问时具备原子性的(long 和 double 是非原子性协定)。
Java 代码中的同步块——synchronized 关键字,在同步块之间的操作具备原子性。
可见性(Visibility)
可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
保证多线程操作时变量的可见性的关键字:volatile、synchronized、final
-
volatile:使得新值能立即同步到主内存,以及每次使用前立即从主内存刷新(普通变量不能保证可见性)。
-
synchronized :对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中。
-
final:被 final 修饰的字段在构造器中一旦初始化完成,那么在其他线程中就可以看见 final 字段的值。
有序性(Ordering)
使用 volatile 修饰的变量可以禁止指令重排序优化,也就是 JMM 中描述的“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics)。
只有一个 CPU 访问内存时,并不需要内存屏障;但如果有两个或更多 CPU 访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性。
-
为何说 volatile 禁止指令重排序?
从硬件架构上讲:指令重排序是指 CPU 采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理(重排序那些指令之间没有依赖的)。
volatile 关键字通过提供“内存屏障”的方式来防止指令被重排序,为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
Java 程序中有序性可以总结为:
如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。
前半句指:“线程内表现为串行的语义”;后半句指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
volatile 和 synchronized 可以保证线程之间操作的有序性。
happens-before 原则
happens-before 原则是指两个操作的偏序关系:如果说操作 A 先行发生于操作 B,其实就是说在发生 B 操作之前,操作 A 产生的影响能被操作 B 观察到,“影响”指修改内存中共享变量的值、发送消息、调用方法等。
如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个之前。
通过一个例子来了解 happens-before:
a = 1; // 线程 A 执行
b = a; // 线程 B 执行
// b 是否等于 1
- 假设线程 A 先于 线程 B 执行,那么 b 一定等于 1;
- 如果两线程之间不存在 happens-before 原则,那么
b == 1
不一定成立,也就是线程 A 对 i 的影响可能会被线程 B 观察到,也可能不会。
时间的先后顺序与 happens-before 之间没有太大的关系。
参考资料:
《深入理解Java虚拟机》
《Java 并发编程的艺术》