1. 引言
考虑到计算机组成的内容:
原始的计算机是CPU用于计算+硬盘用于存储,由于CPU的高速发展和硬盘的缓慢发展,高速的存储需要持续供电且价格昂贵,于是引入了由高速存储组成的内存作为中间的缓冲层。形成了CPU-RAM-Main Memory的金字塔结构。
接下来,由于CPU的继续发展,内存也渐渐跟不上CPU的速度,于是引入了更小更高速的cache作为CPU和内存的缓冲。形成了我们现在熟悉的计算机组成金字塔结构。
然后,由于CPU从单核发展成了多核,计算机从单处理器发展为多处理器,而每个处理器都有自己的cache,而这些高速缓存又要共享同一个主存。为了保证多个缓存中的数据一致性,诞生了很多协议,形成了如下结构。
2. Java内存模型
此时,线程引擎控制着多个线程,就如同实际计算机的多个CPU一样,每个线程有自己的工作内存,而他们最终共享同一个主存。
这里的主存,工作内存与JVM中的堆,栈,方法区不是同一层次内存划分。
3. 内存间的交互操作
Java内存模型定义了八种操作来完成内存与工作内存的具体交互协议:
- lock 锁定,作用于主内存的变量,把一个变量标识为一个线程独占状态。
- unlock 解锁,作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read 读取,作用于主内存的变量,把一个变量从主内存传入现成的工作内存中。
- load 载入,作用于工作内存的变量,把通过read从主内存中得到的变量放入工作内存的变量副本中。
- use 使用,作用与工作内存的变量,把工作内存中一个变量传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个动作。
- assign 赋值,作用于工作内存的变量,把执行引擎中获得的值赋给工作内存中的一个变量,每当虚拟机遇到一个给变量赋值的字节码指令时会执行这个动作。
- store 存储,作用于工作内存的变量,把工作内存中的一个变量值传到主内寸中。
- write 写入,作用于主内存的变量,把通过store从工作内存中得到的变量的值传送到主内存的变量中。
用流程图表示交互操作如下:
如果把一个变量从主存复制到工作内存,就需要按顺序的执行read和load操作。
如果把一个变量从工作内存复制到主存,就需要按顺序的执行store和write操作。
Java内存只要求必须按上述顺序执行操作,没有要求保证操作连续。
Java还规定上述8种操作必须符合如下七条规定:
- 不允许read-load,store-write这两对操作的操作之一单独出现。
- 不允许一个线程丢弃他的最近assign操作,即一个工作内存中的最终变量必须同步到主内存中。
- 在没有发生任何assign操作时,不允许一个线程把工作内存中的变量同步到主内存中。(不允许无原因同步)
- 一个新变量只能在主内存中产生,不允许工作内存直接使用一个未被load或者assign的变量。换言之,执行use前必须load,执行store前必须assign。
- 一个变量同时只允许一个线程对其执行lock操作,lock和unlock必须成对出现。
- 如果一个变量事先没有被执行lock操作,则不能执行unlock操作,也不允许去unlock其他线程的lock操作。
- 一个变量执行unlock前,必须把此变量同步到主内存中,即执行store和write操作。
四. 重排序
由上知八种操作没有连续性要求,但是很多操作是其他操作的前提,操作间满足一定的先序排列。因此,在编译器对代码优化时,往往会通过重排序来优化执行效率。
重排序分为三种类型:
-
编译器优化的重排序
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
-
指令集并行的重排序
现代处理器采用指令集并行技术将多条指令重叠执行,如果不存在数据依赖,处理器可以改变语句对应机器指令的执行顺序。
-
内存系统的重排序
由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从执行到重排序过程如下:
为了保证内存的可见性,Java编译器在生成指令序列的适当位置会插入内存屏障来禁止特定类型的处理器重排序,内存屏障分为以下四种:
- LoadLoad
- LoadStore
- StoreLoad
- StoreStore
参考文章