内存模型

35 阅读6分钟

1. 前提知识

现代计算机中,cpu的计算速度越来越快,相比之下,对存储设备、内存中的数据的读取与写入的效率却迟迟跟不上cpu的计算速度,两者之间的差距越来越大,这也导致了对cpu资源的浪费。比如在进行IO、内存读取的操作时,cpu处于过长的等待状态,这就没有很好的利用到cpu的性能。

- 缓存一致性

为了提高对cpu的利用效率,cpu厂商在每个cpu上加了一个高速缓存,将内存中的数据复制一份到缓存中,待cpu对数据处理完后刷新到内存中去。这就减少了与内存的交互时间,也就是减少了cpu的等待时间,却也引发出缓存一致性问题。

对于多cpu的计算机来说,每个cpu都有各自的缓存,导致同一份数据可能在不同的缓存中的表现不一样。为解决这个问题,规定了各个cpu厂商对cpu的设计必须要遵循MSI、MESI等缓存一致性协议,用来保证数据在各个缓存中表现一致。

计算机内存模型.png

- 指令重排序

同样为了更好的利用cpu的性能,除了给每个cpu增加一个高速缓存外,cpu在不影响运行结果的前提下,允许对有顺序的执行命令进行乱序优化后执行。可是在多个并行计算的情况下,可能会对别的计算结果带来影响,这就是指令重排序带来的问题。

为解决该问题,引入了内存屏障(Memory Barrier)指令,也称为内存栅栏,其中又细分为 读屏障(Load Memory Barrier)、写屏障(Store Memory Barrier)。

对读、写屏障的定义为:

  • 读屏障前的所有读指令执行完毕后,屏障后的读指令才可以执行,写操作不受影响。

  • 写屏障前的所有写指令执行完毕后,屏障后的写指令才可以执行,读操作不受影响。

在读、写屏障的标准下,又分为4种屏障类型:

屏障类型指令示例说明
Load Load BarriersLoad1;Load Load Barriers;Load2;确保数据载入操作Load1执行完毕后,Load2及其之后的载入操作才可以执行。
Store Store BarriersStore1;Store Store Barriers;Store2;确保数据写入操作Store1执行完毕后,Store2及其之后的写入操作才可以执行。
Load Store BarriersLoad1;Load Store Barriers;Store1;确保数据载入操作Load1执行完毕后,Store1及其之后的写入操作才可以执行。
Store Load BarriersStore1;Store Load Barriers;Load1;确保数据写入操作Store1执行完毕后,Load1及其之后的载入操作才可以执行。

2.Java内存模型-JMM(Java Memory Model)

JMM的设计也参考了计算机内存模型,我们都知道Jvm解决的一个重要的问题就是屏蔽各个操作系统底层的差异性,JMM同样也可以屏蔽各硬件和操作系统对内存操作的差异性。

JMM中定义了主内存和工作内存两个内存概念。规定了所有共享对象都储存在主内存中,每个线程都有自己私有的工作内存(工作内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化),当线程需要用到主内存的对象时,从主内存中copy一个对象副本到自己的工作内存中,线程中涉及到该对象的操作都作用到对象副本上,并在完成操作后将修改后的对象副本同步到主内存中的对象上去。

java内存模型.png 对于主内存和工作内存的交互,JMM中定义了8种操作,且规定这8种操作的实现都要是原子的(对于long 和 double类型,在某些平台上,read、load 、store、write 操作允许有例外):

  1. lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  2. unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来。
  3. read(读取):作用于主内存的变量,把一个变量的值从主内存读取到工作内存中,便于load操作使用。
  4. load(载入):作用于工作内存的变量,把read到的变量值放入工作内存的变量副本中。
  5. use(使用):作用于工作内存的变量,把工作内存中的变量值交给执行引擎,每次要使用这个变量值时都会执行 一次该操作。
  6. assign(赋值):作用于工作内存的变量,把从执行引擎接收到的值赋给工作内存中的变量,每次给变量赋值都 会执行一次该操作。
  7. store(储存):作用于工作内存的变量,把工作内存的变量值传给主内存,便于write操作使用。
  8. write(写入):作用于主内存的变量,把store传过来的值放入到主内存的变量中。

而且对这八种操作还必须满足下面几种规则:

  1. 不允许read和load、store和write这两种组合操作中的其中一个操作单独出现,即不允许一个变量从主内存 读取了但是工作内存不接受、或者从工作内存发起回写了但是主内存不接受的情况出现。
  2. 不允许一个线程丢弃他最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步到主内存中去。
  3. 不允许一个线程无原因的(没有发生assign操作)把数据从线程的工作内存同步到主内存中去。
  4. 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化的(load 或 assign操作) 变量,也就是说,对一个变量的use、store操作之前,必须先执行了assign 和 load 操作。
  5. 一个变量在一个时刻内只允许一条线程对它进行lock操作,但lock操作可以被同一个线程重复执行,执行了多少 次的lock操作,只有执行相应次数的unlock操作,该变量才会被解锁。
  6. 对一个变量执行lock操作,会清空工作内存中该变量的值,需要重新load或者assign操作来初始化变量的值。
  7. 一个变量没有被lock操作锁定,不允许对它执行unlock操作,也不允许去unlock一个其他线程lock住的变量。
  8. 一个变量在被lock操作前,必须先把工作内存中的变量值同步到主内存中去,也就是必须执行store 和 write 操作。

当然,jvm是运行在计算机操作系统上的,同样也会涉及到计算机的硬件内存模型所带来的缓存一致性、指令重排序的问题。在学习volatile时再来讨论java是如何解决的。

参考书籍:《深入理解Java虚拟机》周志明