1. 前提知识
现代计算机中,cpu的计算速度越来越快,相比之下,对存储设备、内存中的数据的读取与写入的效率却迟迟跟不上cpu的计算速度,两者之间的差距越来越大,这也导致了对cpu资源的浪费。比如在进行IO、内存读取的操作时,cpu处于过长的等待状态,这就没有很好的利用到cpu的性能。
- 缓存一致性
为了提高对cpu的利用效率,cpu厂商在每个cpu上加了一个高速缓存,将内存中的数据复制一份到缓存中,待cpu对数据处理完后刷新到内存中去。这就减少了与内存的交互时间,也就是减少了cpu的等待时间,却也引发出缓存一致性
问题。
对于多cpu的计算机来说,每个cpu都有各自的缓存,导致同一份数据可能在不同的缓存中的表现不一样。为解决这个问题,规定了各个cpu厂商对cpu的设计必须要遵循MSI、MESI等缓存一致性协议,用来保证数据在各个缓存中表现一致。
- 指令重排序
同样为了更好的利用cpu的性能,除了给每个cpu增加一个高速缓存外,cpu在不影响运行结果的前提下,允许对有顺序的执行命令进行乱序优化后执行。可是在多个并行计算的情况下,可能会对别的计算结果带来影响,这就是指令重排序
带来的问题。
为解决该问题,引入了内存屏障
(Memory Barrier)指令,也称为内存栅栏,其中又细分为 读屏障
(Load Memory Barrier)、写屏障
(Store Memory Barrier),对读、写屏障的定义为:
读屏障前的所有读指令执行完毕后,屏障后的读指令才可以执行,写操作不受影响。
写屏障前的所有写指令执行完毕后,屏障后的写指令才可以执行,读操作不受影响。
在读、写屏障的标准下,又分为4种屏障类型:
屏障类型 | 指令示例 | 说明 |
---|---|---|
Load Load Barriers | Load1;Load Load Barriers;Load2; | 确保数据载入操作Load1执行完毕后,Load2及其之后的载入操作才可以执行。 |
Store Store Barriers | Store1;Store Store Barriers;Store2; | 确保数据写入操作Store1执行完毕后,Store2及其之后的写入操作才可以执行。 |
Load Store Barriers | Load1;Load Store Barriers;Store1; | 确保数据载入操作Load1执行完毕后,Store1及其之后的写入操作才可以执行。 |
Store Load Barriers | Store1;Store Load Barriers;Load1; | 确保数据写入操作Store1执行完毕后,Load1及其之后的载入操作才可以执行。 |
2.Java内存模型-JMM(Java Memory Model)
JMM的设计也参考了计算机内存模型,我们都知道Jvm解决的一个重要的问题就是屏蔽各个操作系统底层的差异性,JMM同样也可以屏蔽各硬件和操作系统对内存操作的差异性。
JMM中定义了主内存和工作内存两个内存概念。规定了所有共享对象都储存在主内存中,每个线程都有自己私有的工作内存(工作内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化
),当线程需要用到主内存的对象时,从主内存中copy一个对象副本到自己的工作内存中,线程中涉及到该对象的操作都作用到对象副本上,并在完成操作后将修改后的对象副本同步到主内存中的对象上去。
对于主内存和工作内存的交互,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虚拟机》周志明