JVM内存模型(Java Memory Model)
简要言之,jmm是jvm的一种规范,定义了jvm的内存模型。它屏蔽了各种硬件和操作系统的访问差异,它的主要目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。
主内存和工作内存之间的交互
操作 | 作用对象 | 解释 |
---|---|---|
lock | 主内存 | 把一个变量标识为一条线程独占的状态 |
unlock | 主内存 | 把一个处于锁定状态的变量释放出来,释放后才可被其他线程锁定 |
read | 主内存 | 把一个变量的值从主内存传输到线程工作内存中,以便 load 操作使用 |
load | 工作内存 | 把 read 操作从主内存中得到的变量值放入工作内存中 |
use | 工作内存 | 把工作内存中一个变量的值传递给执行引擎, 每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行这个操作 |
assign | 工作内存 | 把一个从执行引擎接收到的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 |
store | 工作内存 | 把工作内存中的一个变量的值传送到主内存中,以便 write 操作 |
write | 工作内存 | 把 store 操作从工作内存中得到的变量的值放入主内存的变量中 |
缓存一致性:每条线程都有自己的工作内存,里面保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量,相似的,Java虚拟机也定义了一套内存访问协议来保证内存一致性;
指令重排序:对应于处理器乱序执行,Java虚拟机的即时编译器中也有着类似的优化,同样只保证最终结果的一致性。
对于 volatile 型变量的特殊规则
关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。
一个变量被定义为 volatile 的特性:
1.保证此变量对所有线程的可见性。但是操作并非原子操作,并发情况下不安全。
2.禁止指令重排序优化。
通过插入内存屏障保证一致性。
内存屏障:指令重排序时不能把内存屏障之后的指令重排序到内存屏障之前。
volatile使用场景:
1.运算结果并不依赖变量的当前值(如作为状态变量),或仅有单一线程修改变量值
2.变量不与其他状态变量共同参与不变约束(因为对其中一个变量值赋值之后并在对其他变量赋值之前,不变约束可能失效)
如果不能同时满足上述两个条件,则必须使用锁(synchronize同步代码块锁)来保证并发安全。
对于 long 和 double 型变量的特殊规则
Java 要求对于主内存和工作内存之间的八个操作都是原子性的,但是对于 64 位的数据类型,有一条宽松的规定:允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作划分为两次 32 位的操作来进行,即允许虚拟机实现选择可以不保证 64 位数据类型的 load、store、read 和 write 这 4 个操作的原子性。这就是 long 和 double 的非原子性协定。
原子性、可见性与有序性
原子性(Atomicity)
在并发编程中,如果某操作具备整体性,也就是说,系统其他部分无法观察到其中间步骤所生成的临时结果,而只能看到操作前与操作后的结果,那么该操作就是“原子的”(atomic)。
由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write。大致可以认为基本数据类型的操作是原子性的。同时 lock 和 unlock 可以保证更大范围操作的原子性。
可见性(Visibility)
是指当一个线程修改了共享变量的值,其他线程也能够立即得知这个通知。
主要操作细节就是修改值后将值同步至主内存(volatile 值使用前都会从主内存刷新),除了 volatile 还有 synchronize 和 final 可以保证可见性。
同步块的可见性是由“对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中( store、write 操作)”这条规则获得。
而 final 可见性是指:被 final 修饰的字段在构造器中一旦完成,并且构造器没有把 “this” 的引用传递出去( this 引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final 字段的值。
有序性(Ordering)
如果在线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。
前半句指“线程内表现为串行的语义”,后半句是指“指令重排”现象和“工作内存与主内存同步延迟”现象。
Java 语言通过 volatile 和 synchronize 两个关键字来保证线程之间操作的有序性。
volatile 自身就禁止指令重排,而 synchronize 则是由“一个变量在同一时刻指允许一条线程对其进行 lock 操作”这条规则获得,这条规则决定了持有同一个锁的两个同步块只能串行的进入。
先行发生原则(happens-before)
这个原则是判断数据是否存在竞争、线程是否安全的主要依据。先行发生是 Java 内存模型中定义的两项操作之间的偏序关系。
天然的先行发生关系:
规则 | 解释 |
---|---|
程序次序规则 | 在一个线程内,代码按照书写的控制流顺序执行 |
管程锁定规则 | 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作 |
volatile 变量规则 | volatile 变量的写操作先行发生于后面对这个变量的读操作 |
线程启动规则 | Thread 对象的 start() 方法先行发生于此线程的每一个动作 |
线程终止规则 | 线程中所有的操作都先行发生于对此线程的终止检测 (通过 Thread.join() 方法结束、 Thread.isAlive() 的返回值检测) |
线程中断规则 | 对线程 interrupt() 方法调用优先发生于被中断线程的代码检测到中断事件的发生 (通过 Thread.interrupted() 方法检测) |
对象终结规则 | 一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始 |
传递性 | 如果操作 A 先于 操作 B 发生,操作 B 先于 操作 C 发生,那么操作 A 先于 操作 C |