Memory Model -- 14 -- Java内存模型
在计算机中,绝大多数的运算任务不可能只靠处理器的计算就能完成,处理器需要与内存进行交互,如:读取运算数据、存储运算结果等,这个 I/O 操作是很难消除的 (无法仅靠寄存器来完成所有运算任务)
由于计算机的存储设备与处理器的运算速度有着几个数量级的差距,所以在现代计算机系统中都不得不加入一层或多层读写速度尽可能接近处理器运算速度的高速缓存 (Cache) 来作为内存与处理器之间的缓冲:将运算需要使用的数据复制到缓冲中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了
当多个处理器的运算任务都涉及到同一块主存区域 (Main Memory) 时,将可能导致各自的缓存数据不一致,因此各个处理器访问缓存时都需要遵守一些协议,在读写时要根据协议来进行操作,如:MSI、MESI(Illinois Protocol)、MOSI、 Synapse、Firefly 及 Dragon Protocol
由此衍生出了内存模型的概念,即在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象
处理器、高速缓存、主内存间的交互关系如下
一、Java内存模型 (Java Memory Model / JMM)
-
Java 内存模型主要用于屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果
-
Java 内存模型是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,用于定义程序中各种变量 (包括实例字段、静态字段和构成数组对象的元素) 的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节
-
Java 内存模型规定了所有的变量都存储在主内存 (Main Memory) 中,每个线程有其各自的工作内存 (Working Memory)
-
线程工作内存中保存了被该线程使用的变量的主内存副本
-
线程对变量的所有操作 (读取、赋值等) 都必须在工作内存中进行,而不能直接读写主内存中的数据
-
不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成
-
-
线程、主内存、工作内存三者的交互关系如下
-
Java 内存模型与 Java 内存区域划分 (运行时数据区) 是两个不同的概念,没有任何关系
二、内存间的交互操作
-
关于主内存和工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的实现细节,Java 内存模型中定义了以下 8 种操作 (在最新的 JSR-133 文档中,将 Java 内存模型的操作简化为 read、write、lock 和 unlock 四种,但只是语言描述上的等价简化, Java 内存模型的基础设计并未改变)
-
lock (锁定)
- 作用于主内存的变量,将一个变量标识为一个线程独占的状态
-
unlock (解锁)
- 作用于主内存的变量,将一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
-
read (读取)
- 作用于主内存的变量,将一个变量的值从主内存传输到线程的工作内存中,以边随后的 load 操作使用
-
load (载入)
- 作用于工作内存的变量,将 read 操作从主内存中得到的变量值放入工作内存的变量副本中
-
use (使用)
- 作用于工作内存的变量,将工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
-
assign (赋值)
- 作用于工作内存的变量,将一个从执行引擎接收的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作
-
store (存储)
- 作用于工作内存的变量,将工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用
-
write (写入)
- 作用于主内存的变量,将 store 操作从工作内存中得到的变量的值放入主内存的变量中
-
三、针对 long 和 double 类型变量的特殊规则
-
Java 内存模型要求 lock、unlock、read、load、assign、use、store、write 这 8 种操作都具有原子性,但是对于 64 为的数据类型 (long 和 double),在模型中特别定义了一条宽松的规定:允许虚拟机将没有被
volatile
修饰的 64 位数据的读写操作划分为两次 32 位的操作来进行,即允许虚拟机实现自行选择是否要保证 64 位数据类型的 load、store、read 和 write 这个 4 个操作的原子性,这就是所谓的 “long 和 double 的非原子性协定” (Non-Atomic Treatment of double and long Variables) -
在目前主流平台下的 64 位 Java 虚拟机中并不会出现非原子性访问行为,但是对于 32 位的 Java 虚拟机 (如:常用的 32位 x86 平台下的 HotSpot 虚拟机),对 long 类型的数据确实存在非原子性访问的风险
-
从 JDK9 开始,HotSpot 增加了一个实验性的参数
-XX:+AlwaysAtomicAccesses
来约束虚拟机对所有数据类型进行原子性的访问,针对 double 类型而言,由于现代 CPU 中一般都包含专门用于处理浮点数据的浮点运算器,用来专门处理单、双精度的浮点数据,所以哪怕是 32 位虚拟机中通常也不会出现非原子性访问的问题 -
因此在实际开发中,除非该数据明确可知的线程竞争,否则我们在编写代码的时候一般不需要因为这个原因而刻意把用到的 long 和 double 变量专门声明为 volatile
四、Java 内存模型特点
-
原子性 (Atomicity)
-
由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write 这 6 个,因此我们大致可以认为,基本数据类型的访问、读写都是具备原子性的 (例外就是 long 和 double 的非原子性协定)
-
如果应用场景需要一个更大范围的原子性保证,Java 内存模型还提供了 lock 和 unlock 操作来满足这种需求,尽管并没有直接开放给用户使用,但却提供了更高层次的字节码指令
monitorenter
和monitorexit
来隐式地使用这两个操作,这两个字节码指令反映到 Java 代码中就是同步块,即synchronized
关键字,因此在 synchronized 块之间的操作也具备原子性
-
-
可见性 (Visibility)
-
可见性是指当一个线程修改了共享变量的值时,其他线程能立即得知这个修改
-
在 Java 内存模型中,我们可以通过
volatile
、synchronized
、final
这 3 个关键字来实现可见性-
volatile
- volatile 保证了在修改变量后能立即将新值同步回主内存,以及每次读取变量前立即从主内存中刷新变量值
-
synchronized
- 对一个变量执行
unlock
操作 (monitorexit
字节码指令中隐式使用) 之前,必选先把此变量同步回主内存中 (执行store
、write
操作)
- 对一个变量执行
-
final
- 被
final
修饰的字段在构造器中一旦被初始化完成,并且构造器没有把 “this” 的引用传递出去 (this 引用逃逸可能会导致其他线程通过它访问到 “初始化了一般” 的对象) ,那么在其他线程中就能看见 final 字段的值
- 被
-
-
-
有序性 (Ordering)
-
Java 语言中提供了
volatile
和synchronized
两个关键字来保证线程之间操作的有序性-
volatile
-
volatile
本身包含了禁止指令重排序的语义
-
-
synchronized
- 一个变量在同一个时刻只允许一个线程对其进行
lock
操作 (monitorenter
字节码指令中隐式使用),持有同一个锁的两个同步块只能串行地进入
- 一个变量在同一个时刻只允许一个线程对其进行
-
-
五、先行发生原则 (Happens-Before)
-
先行发生原则 (Happens-Before) 是判断数据是否存在竞争,线程是否安全的重要手段
-
先行发生关系 无须任务同步器协助就已经存在,可以在编码中直接使用,如果两个操作之间的关系不在下列规则内,并且无法从下列规则推导出来,则它们就没有顺序性的保证,虚拟机可以对它们随意地进行重排序
-
程序次序规则 (Program Order Rule)
- 在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作 (是控制流顺序,而不是程序代码顺序,因为考虑分支、循环等结构)
-
管程锁定规则 (Monitor Lock Rule)
- 一个
unlock
操作先行发生于后面对同一个锁的lock
操作 (必须强调是同一个锁,“后面” 指的是时间上的先后)
- 一个
-
volatile 变量规则 (Volatile Variable Rule)
- 对一个
volatile
变量的写操作先行发生于后面对这个变量的读操作 (“后面” 指的是时间上的先后)
- 对一个
-
线程启动规则 (Thread Start Rule)
- Thread 对象的 start() 方法先行发生于此线程的每一个动作
-
线程终止规则 (Thread Termination Rule)
- 线程中的所有操作都先行发生于对此线程的终止检测 (可以通过 Thread::join() 方法是否结束、Thread::isAlive() 方法的返回值等手段检测线程是否已经终止执行)
-
线程中断规则 (Thread Interruption Rule)
- 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生 (可以通过 Thread::interrupted() 方法检测到是否有中断发生)
-
对象终结规则 (Finalizer Rule)
- 一个对象的初始化完成 (构造函数执行结束) 先行发生于它的 finalize() 方法的开始
-
传递性 (Transitivity)
- 如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C
-
-
时间先后顺序与先行发生原则之间基本没有因果关系,所以当衡量并发安全问题的时候不要受时间顺序的干扰,一切必须以先行发生原则为准
六、归纳总结
-
主内存
-
存储 Java 实例对象,包括成员变量、类信息、常量、静态变量等
-
属于数据共享的区域,多线程并发操作时会引发线程安全问题
-
-
工作内存
-
存储被当前线程使用的变量的主内存副本,对其他线程不可见
-
属于线程私有数据区域,不存在线程安全问题
-
-
Java 内存模型特点
-
原子性
- 一个操作或多个操作要么全部执行,要么全部不执行
-
可见性
- 当多个线程同时访问同一个变量时,一个线程修改了变量的值,其他线程能立即看得到修改的值
-
有序性
- 程序执行的顺序按照代码的先后顺序执行
-
-
指令重排序需要满足的条件
- 无法通过 happens-before 原则推导出来的,才能进行指令的重排序
-
先行发生原则
- 用于判断数据是否存在竞争,以及线程是否安全
-
先行发生关系
-
程序次序规则
-
管程锁定规则
-
volatile 变量规则
-
线程启动规则
-
线程终止规则
-
线程中断规则
-
对象终结规则
-
传递性
-