当我们在聊java内存模型的时候,我们在聊些什么
内存结构和内存模型
很长的一段时间里面,当提起java内存模型的时候,一直以为是在聊堆区栈区年轻代,年老代,永久代。
这里为看此文的朋友提个醒,堆栈区这些一般是指内存机构或者内存区域,当我们聊java内存模型的时候,更多的是以下图展开
再看看计算机内存模型。
java内存模型是基于传统计算机硬件内存模型的抽象
JMM中多线程运行即是:硬件内存模型中多核CPU的并行工作。
JMM中工作内存相当于:各组CPU的一二级缓存,寄存器的抽象。
JMM中关注的同步可见问题即:计算机各级缓存和主内存的一致性问题。
java是跨平台的语言,设计初衷是不依赖某一种计算机硬件体系,所以有了在硬件体系之上抽象出的内存模型。
从 Java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
编译器优化的重排序:在不改变单线程程序语义的情况下,调整语句执行的次序。优点是在编译阶段精简语句,减少不必要资源(内存等)访问。
指令级别的重排序:现代处理器采用指令并行技术使指令重叠执行。优点是发挥同时工作提高执行效率。
内存系统的重排序:由于处理器使用的缓存和读写缓冲区,使得加载和存储可能看起来是在乱序执行。优点是利用可高效读写的缓存提高整体读写效率。
不过重排序在提高多线程执行效率的同时也造成了数据同步内存可见性问题,如下场景
上图中语句最终执行结果可能造成:x=0 , y=0 的结果。原因是a b在内存中初始化值为0,在赋值语句A1和B1执行后,新的值只在缓存中更新并未刷新到内存。导致A2 B2从内存中取出旧数据。
由此见重排序虽好,但也需要遵循一定的原则,来推导什么情况下可以重排序,什么情况下不能重排序。即下面的Happes-Before原则。
如果一个操作执行结果需要对另一个操作可见,那么两个操作之间存在Happens-before关系,这里提到的两个操作既可以在同一个线程里,也可以在不同线程之间。
与程序员密切相关的happens-before规则如下
程序顺序规则:一个线程中的每个操作,happens-before于线程中的任意后续操作。
监视器锁规则:一个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
规则有了,具体怎么对重排序的控制? 就用到了内存屏障。
内存屏障使得屏障指令两边的指令序列不能进行某种类型的重排序。 内存屏障分为两种: Load Barrier 和 Store Barrier即读屏障和写屏障。
读屏障: 使缓存失效,使下一条指令强制从主存中读取最新的值。
写屏障: 使写入缓存的数据立刻同步刷新到主存中,让其他线程可见。
JMM对内存屏障细分为四种
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量。
为了实现volatile的语义,编译器在生成字节码时在指令序列中插入内存屏障。
在每一个volatile写操作前面插入一个StoreStore屏障
在每一个volatile写操作后面插入一个StoreLoad屏障
在每一个volatile读操作后面插入一个LoadLoad屏障
在每一个volatile读操作后面插入一个LoadStore屏障
StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。
LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
简单理解为:
当程序执行到 volatile 变量的读操作或写操作时: 在其前面的操作肯定全部已经完成,且结果对后面的操作可见。
指令重排序优化时,不能将 volatile 变量前面的语句放在其后面执行,也不能将 volatile 变量后面的语句放到其前面执行。