JVM -Java内存模型以及内存屏障深入解读(Java merory model)

1.Java内存模型

1.1主内存与工作内存

java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量存储到内存和从内存中取出变量值这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的不会被共享,自然就不会存在竞争问题。

主内存(Main memory):此处的主内存与物理硬件得主内存名字一样,两者也可以互相类比,但此处仅是虚拟机内存得一部分。

工作内存(Working Memory ):每条线程还有自己的工作内存(可与高速缓存类比),线程的工作内存保存了被该线程使用到的变量的主内存得副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量的值传递均需要通过主内存来完成。

这里说的主内存、工作内存与JVM中Java堆、栈、方法区等并不是一个层次的内存划分,如果勉强对比,从变量、主内存、工作内存定义来看,主内存主要对应Java堆中的对象实例数据部分,而工作内存则对应虚拟机栈中的部分区域。

1.2 内存间交互操作

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存得实现细节。Java内存模型定义了8种操作来完成,且虚拟机实现时必须保证是原子的、不可再分的(double,long 类型除外)。

  • lock(锁定):作用与主内存得变量,它把一个变量标识为一跳线程独占状态

  • unlock(解锁):作用于主内存得变量,它把一个处于锁定状态的变量释放出来,释放后得变量才可以被其他线程锁定

  • read(读取):作用与主内存得变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用

  • load(载入):作用于工作内存得变量,它把read操作从主内存中得到的变量值放入到工作内存得变量副本中。

  • use(使用):作用于工作内存得变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作

  • assign(赋值):作用于工作内存得变量,它把一个从执行引擎收到的值赋值给工作内存得每个变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  • store(存储):作用于工作内存得变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用

  • write(写入):作用于主内存得变量,它把store操作从工作内存得到的变量的值放入到主内存得变量中。

      如果要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作,反之如果要将工作内存得变量同步回主内存,就要顺序得执行store,write操作。注意:这里说的是这两个操作必须顺序执行,而并不要求是连续操作,也就是说read,load之间,store,wirte之间是可以插入其他指令得,如堆内存变量a,b进行访问时,一种可能得顺序是read a 、read b,load b 、load a,除此之外,Java内存模型还规定了在执行上诉8中基本操作时必须要满足如下规则:

  • 不允许read 和load ,store 和write操作之一单独出现,即不允许一个变量从主内存存读取了但是工作内存不接受或者从工作内存发起回写了但主内存不接受的情况出现

  • 不允许一个线程丢弃它得最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

  • 不允许一个线程无原因地(没有发生过任何assign操作),把数据从线程的工作内存同步回主内存中

  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load 或者assign)的变量。换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。

  • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁

  • 如果一个变量执行lock操作,那将回清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load 或者assign操作初始化变量的值。

  • 如果一个变量事先没有被lock操作锁定,那就不允许对他执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。

  • 对一个变量执行unlock操作前,必须把该变量同步回主内存中(store,write操作)

1.3volatile 型变量的特殊规则

被volatile修饰的变量具有两层特性

  • 可见性:当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的,而普通变量是做不到这一点的。普通变量的值在线程间传递均需通过主内存来完成,例如线程A修改一个普通变量值后项主内存进行回写,另一个线程B在线程A回写完成了之后再从主内存读取操作,新变量值才会堆线程B可见。

关于voatile变量的可见性,经常回被误解为“volatile 变量对所有线程是立即可见的,对volatile变量所有写操作都能立刻反应到其他线程中,换句话说volatile变量在各个线程中是一致的,所以volatile变量的运算在并发下是安全的”。这句话论据没有错,但是并不能得出这样的结论,原因是Java里面的运算并非原子性操作,导致volatile变量也可以在并发下一样是不安全的。

由于volatile变量只能保证变量的当前值,在不符合以下两条规则运算场景中,我们依然要通过加锁来确保原子性。

  • 运算结果并不不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值

  • 变量不需要与其他的状态变量共同参与不变约束

  • 禁止指令重排序优化:普通变量仅仅保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作得顺序与程序代码的执行顺序一致,因为在一个线程的方法执行过程中无法感知到这一点,这也就是java 内存模型中描述的所谓的“线程内表现未串行的语义”

1.4重排序

在执行程序时为了提高性能、编辑器和处理器经常会对指令进行重排序。重排序分为三种类型:

  • 编辑器优化的重排序。编译器在不改变单线程语义前提下,可以重新安排语句的执行顺序。
  • 指令级别的重排序:现在处理器采用了指令级并行技术来将多条指令重叠执行,如果不存在数据依赖性,处理器可以该你那吗语句对应机器指令的执行顺序。
  • 内存系统的重排序:由于处理器使用缓存和读写缓冲区,这使得家在和存储看上去可能是在乱序执行/

Java源代码到最终执行的指令顺序,会经过下面三种重排序:

为了保证内存的可见性,Java编译器在生成指令序列的适当位置插入内存屏障来禁止特定类型的处理器重排序。Java内存模型把内存屏障分为LoadLoad、LoadStore、StoreLoad、StoreStore四种。

屏障类型 指令示例 说明
LoadLoad Barriers Load 1;LoadLoad;Load2 确保Load1数据的装载,之前与Load2以及所有后续装载指令的装载
StoreStore Barries

Store1;StoreStore;

Store2

确保Store数据对其他处理器可见(刷到内存),之前于Store2以及后续存储指令的存储
LoadStore Barriers

Load1;

LoadStore;

Store2

确保Load1数据装载,之前与Store2以及所有后续的存储指令刷新到内存
StoreLoad Barriers

Store2

StoreLoad

Load2

确保Store1数据对其他处理器变得可见(指刷新到内存),之前与Load2以及所有后续装载指令的装载。StoreLoad Barrires 会使改屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。

 

猜你喜欢

转载自blog.csdn.net/yueyazhishang/article/details/106173900