Java虚拟机规范中定义一种Java内存模型来屏蔽各种硬件和操作系统的内存访问差异,以实现Java在各种平台都能达到一致的内存访问效果。
引发问题:
内存模型必须保证多线程并发内存访问不会出现问题。
定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中读取变量的底层细节,此处的变量不包含局部变量与方法参数,因为他们是线程私有的,不会被共享,也不存在线程之间的可见性问题,所以不受内存模型影响。
Java内存模型规定所有变量都存储在住内存(虚拟机内存的一部分)中,每条线程都有自己的工作内从。线程的工作内存中保存了被该线程使用到的变量的住内存的副本,线程对变量的所有操作都必须在自己的工作内存中,不同的线程也无法访问对方的工作 内存。线程间变量值的传递必须通过住内存。
JMM抽象结构模型
CPU的处理速度和主存的读写速度不是一个量级的,为了平衡这种巨大的差距,每个CPU都会有自己的高速缓存。类比,共享变量会先放在主存中,每个线程都有属于自己的工作内存,并且会把位于主存中的共享变量拷贝到自己的工作内存,之后的读写操作均使用位于工作内存的变量副本,并在某个时刻将工作内存的变量副本写回到主存中去,下图是线程 ,工作内存,主内存三者交互关系图
内存间交互操作
JMM定义了8中澳做来完成变量 在工作内存和主内存之间的传递,虚拟机在实现时必须保证这8种操作都是原子的不可再分的。
命令 | 作用区域 | 描述 |
---|---|---|
lock 锁定 | 主内存 | 将一个变量标识为一条线程独占状态 |
unlock 解锁 | 主内存 | 将一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程使用 |
read 读取 | 主内存 | 将主内存的变量值传输到工作内存,以便之后load操作 |
load 载入 | 工作内存 | 将read从主内存读取过来的变量值放入到工作内存的变量副本中 |
use 使用 | 工作内存 | 将工作内存中的一个变量值传递给执行引擎 每当虚拟机遇到一个需要使用变量值的字节码指令时会执行这个操作 |
assign 赋值 | 工作内存 | 将从执行引擎接收到的变量值赋给工作内存的变量 每当虚拟机遇到一个给变量赋值的字节码指令时会执行这个操作 |
store 存储 | 工作内存 | 将工作内存中的变量值传送到主内存,以便之后write操作 |
write 写入 | 主内存 | 将store从工作内存中得到的变量值放到主内存的变量中 |
如果要把一个变量从主内存复制到工作内存中,则必须顺序的执行read 和load操作,如果把变量值从工作内存中同步回主内存,则必须顺序的执行store 和 write操作,值得注意的是Java内存模型只规定了这两个操作必须按照顺序执行,但是没有保证连续执行。也就是,read load,store write 之间是可以插入其他指令的。除此之外,JMM还规定了执行上述8种操作必须满足如下规则:
- 一个变量同一时刻只允许一个线程对其进行lock操作,但同一个线程可以对同一个变量进行多次lock,并且需要执行相应数量的unlock操作,才能释放该变量的锁。
- 一个变量被lock之后,会清空工作内存中的变量副本,在执行引擎使用这个变量之前,需要重新执行load或assigin操作初始化该变量的值
- 一个变量没有被lock,则不允许被unlock,也不允许unlock其他线程锁定的变量
- 一个变量在执行unlock之前,必须先同步到主内存,即执行了store操作
- 不允许 read load store write 单一出现
- 变量必须从主内存诞生,不允许在工作内存中直接使用未被初始化的变量
- 变量不允许没有任何原因的写回到主内存,即store之前必须执行过assgin操作
- 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须同步到主内存中
原子性、可见性与有序性
Java内存模型是围绕着在并发过程中如何处理 原子性,可见性和有序性这三个特征来建立的
-
原子性
一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性
由JMM来直接保证原子性变量包括read、load、assigin、use、store、write,我们大致可以认为基本数据类型的访问读写是具备原子性的(double 和long 是非原子性协定)。
如果一个应用需要更大范围的原子性保证,JMM提供了更高层次的字节码指令monitorenter与monitorexit 隐式的使用 lock与unlock操作,而这两个字节码指令映射到java代码中就是synchronized关键字,因此 synchronized块中的操作也具备原子性。
可见性
可见性指一个线程修改了共享变量的值,另一个线程能够立刻得知这个修改
Java中 volatile ,synchronized ,final 关键字能够实现可见性。
volatile实现可见性:volatile共享变量保证在工作内存中改变后的新值能够立刻写回到主存中,并且会使其他cpu里缓存了该内存地址的数据无效(在多处理器下,为了保证各个处理器的缓存一致,需要实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的有效性,当发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存设置成失效,当再次使用该变量的时候就会从主存读到工作内存)
synchronized实现可见性:synchronized变量同一时刻只允许同一线程进行访问,在执行unlock操作前必须执行write操作,在执行lock后将工作内存中的该变量失效,在使用前从主存中获取。
final实现可见性:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去,那其他线程就可以看到final字段的值。有序性
在本线程内观察,所有操作都是有序的,即线程内表现为串行的语义
在一个线程中观察另一个线程,所有操作都是无序的,指指令重排和工作内存,主内存同步延迟现象
Java中 volatile 与 synchronized 关键字实现了有序性
volatile 关键字禁止指令重排
synchronized 一个变量同一时刻只允许一条线程对其进行lock操作。
先行发生原则
如果A操作先行发生与B操作,则A操作的发生的影响能被B观察到。
如果线程A先行于B,B先行于C,则A先行于C。