Chapter 12.3 - Java 内存模型
Created by : Mr Dk.
2020 / 02 / 03 16:53
Ningbo, Zhejiang, China
12.1 概述
并发处理是 Amdahl 定律 代替 摩尔定律 成为计算机性能发展源动力的根本原因。计算机的运算速度与其存储和通信子系统的速度差距太大,需要使用一些手段将 CPU 的运算能力压榨出来。
12.2 硬件的效率与一致性
现代计算机系统加入一层或多层读写速度尽可能接近 CPU 的 cache 作为内存与 CPU 之间的缓冲,引入了新的问题:缓存一致性 (Cache Coherence):在多路 CPU 中,每个 CPU 有自己的 cache,而它们又共享同一主内存 (Main Memory),这类系统被称为 共享内存多核系统 (Shared Memory Multiprocessors System)。当多个 CPU 的运算任务涉及同一块主存区域时,各 CPU 访问 cache 时需要遵循一定的协议。
此外,CPU 可能对输入代码进行 乱序执行 (Out-Of-Order Execution)。CPU 会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果一致,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。JVM 的 JIT 编译器中也有指令重排序优化。
12.3 Java 内存模型
JVM 规范试图定义一种 Java 内存模型 (Java Memory Model, JMM),以屏蔽各种硬件和 OS 的内存访问差异,实现让 Java 程序在各平台下都能达到一致的访存效果。而不像 C/C++ 直接使用物理硬件和 OS 的内存模型。
- JMM 必须定义得足够严谨,让并发访问操作不会产生歧义
- JMM 也必须定义得足够宽松,使 JVM 实现能够自由利用硬件或 OS 的特性来获得更好的执行速度
12.3.1 主内存与工作内存
JMM 的主要目的是定义程序中各种变量的访问规则,即关注 JVM 把变量存储到内存和从内存中取出这样的底层细节。
- 实例字段
- 静态字段
- 构成数组对象的元素
不包括局部变量和函数参数,因为这是线程私有的,不会被共享:
- 局部变量是 reference 类型本身位于 Java 栈的局部变量表中
- 引用的对象位于 Java Heap,可被各个线程共享
JMM 规定,所有的变量都存储在 主内存 (Main Memory) 中,每条线程有自己的 工作内存 (Working Memory):
- 工作内存中保存了该线程使用的变量的主内存副本
- 线程对变量的读写都必须在 工作内存 中进行
- 不同线程之间无法直接访问对方工作内存中的变量,需要通过主内存传递
JVM 可能会让工作内存优先存储于寄存器和 cache 中。
12.3.2 内存间交互操作
对于主内存与工作内存的交互协议。JMM 定义了 8 种操作,JVM 的实现必须保证每一种操作都是原子的、不可再分的:
lock
- 将主内存中的变量标识为某个线程独占unlock
- 将主内存中的变量释放锁定read
- 将主内存中的变量值传输到线程工作内存中load
- 把从主内存中得到的变量值放入工作内存的副本中use
- 把工作内存中的变量值转递给执行引擎assign
- 把从执行引擎中接收的值赋值给工作内存变量store
- 把工作内存中的变量传送到主内存中write
- 把从工作内存中得到的变量放入主内存的变量中
主内存 ⇔ 工作內存 ⇔ 执行引擎
- 要把一个变量从主内存拷贝到工作内存,就要按顺序执行
read
和load
- 要把一个变量从工作内存同步回主内存,就要按顺序执行
store
和write
- 只要求按顺序执行,不要求连续执行
JMM 规定了执行以上基本操作时必须满足的规则:
read
+load
和store
+write
必须成对出现 - 主内存 ⇔ 工作内存- 不允许线程丢弃最近的
assign
操作,变量在工作内存中改变后必须同步回主内存 - 不允许线程无原因地 (没有发生
assign
) 把工作内存同步到主内存 - 新的变量只能在主内存中诞生
- 一个变量在同一时刻只允许被一个线程
lock
,但lock
可以被同一条线程重复执行多次,并需要相同次数的unlock
- 如果对一个变量执行
lock
,则清空工作内存中该变量的值 - 如果一个变量没有被
lock
,则不允许unlock
;不允许unlock
一个被其它线程锁定的变量 - 对一个变量
unlock
之前,必须先将其同步回主内存中
12.3.3 对于 volatile 型变量的特殊规则
关键字 volatile
是 JVM 提供的最轻量级的同步机制,当一个变量被定义为 volatile
后,将具备两个语义:
- 保证此变量对所有线程的可见性
- 禁止指令重排序优化
可见性:当一条线程修改了变量值,新值对其它线程来说可以立刻得知。普通变量不能完成可见性,值在线程间传递时需要通过主内存完成。从物理存储角度,各线程的工作内存中 volatile
变量也可以存在不一致的情况,但由于每个线程在使用之前都要刷新该值,执行引擎看不到这种不一致的情况。
由于 volatile
变量的运算在并发时不是原子操作,因此同样是线程不安全的。在不符合以下两条规则的运算场景中,仍然需要通过加锁来保证原子性:
- 运算结果不依赖当前的变量值,或确保只有单一线程修改变量值
- 变量不需要其它的状态变量共同参与不变约束
什么玩意儿...这么抽象 😪
指令重排序后:
- 普通变量仅会保证函数执行过程中所有依赖赋值结果的地方都能获取正确的结果
- 不能保证变量赋值与程序代码中的执行顺序一致
CPU 允许多条指令不按程序规定的顺序分发给各个电路单元进行处理,且必须保证程序能够得出正确的结果。指令重排序无法越过内存屏障。综上,volatile
的读性能与普通变量基本没有区别;写操作上可能会慢一些,因为插入了很多内存屏障。JMM 对 volatile
变量定义的规则:
- 在工作内存中,每次使用变量前需要从主内存中刷新最新的值,保证能够看见其它线程对变量的修改
- 每次修改变量后必须立刻同步回主内存中
- 不会被指令重排序优化
12.3.4 针对 long 和 double 型变量的特殊规则
JMM 对于 64-bit 的数据类型的规定较为宽松,允许非 volatile
的变量拆分为两次 32-bit 的操作进行,由 JVM 实现自行决定是否保证 64-bit 数据类型的操作原子性。基本上非原子性访问行为的安全问题不会发生。
12.3.5 原子性、可见性与有序性
JMM 如何在并发过程中处理原子性、可见性和有序性。
12.3.5.1 原子性 (Atomicity)
基本数据类型的访问、读写都是原子的。JVM 未把 lock
和 unlock
直接开放给用户,但提供了更高层次的字节码指令 monitorenter
和 monitorexit
。反映到 Java 代码中就是 synchronized
关键字。
12.3.5.2 可见性 (Visibility)
当一个线程修改了共享变量的值时,其它线程能够立即得知这个修改:
- 变量修改后立即同步回主内存
- 变量读取前从主内存刷新
12.3.5.3 有序性 (Ordering)
在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。
?