<<深入理解java内存模型>>学习总结

硬件内存模型

我们知道计算机处理器的运算速度远远高于它的存储和通信子系统的速度,大量的时间都花费在磁盘I/O,网络通信和数据库访问上。处理器运算时候,读取和存取数据必定需要与内存进行I/O交互。而对主存的访问是一项昂贵的操作,内存成了CPU的性能限制,这称为Memory Wall。所以不得不在处理器和内存之间引入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)。简单来说就是在缓存中保存一份热点数据的拷贝供CPU尽快存取。这时又引入了另一个问题:缓存一致性(Cache coherency)。在多处理器系统中,每个CPU都有自己的高速缓存,而它们又共享同一主内存,当多个CPU运算任务同时设计主内存中同一块区域时,可能会导致缓存数据的不一致性。为了解决缓存一致性的问题,CPU访问缓存时要遵守一些缓存一致性协议。常用的缓存一致性协议有MESI(Modified Exclusive Shared Or Invalid)。想要了解MESI可以参考这篇博文 http://blog.csdn.net/realxie/article/details/7317630 。

除了高速缓存以外,为了使处理器内部的运算单元尽可能的充分被利用,处理器可能还会对输入代码进行乱序执行,只要该计算结果与程序代码顺序执行结果一致就行。这就不得不提及几个概念——重排序与内存屏障(Memory barrier)。下文将具体描述。

Java内存模型抽象

Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。可见本地内存的概念同上述处理器的高速缓存相似。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
线程之前的通信过程如下图所示:

java内存模型基于共享内存机制,可见,线程之间的通信为以下过程:
1、线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2、线程B从主内存中读取共享变量的最新值。

数据依赖性

若两个操作访问同一个变量,且这两个操作其中之一为写操作,此时,这两个操作之间便存在数据依赖性。数据依赖性分为以下三种:
1、写后读 如 a=1; b=a;
2、写后写 如 a=1; a=2;
3、读后写 如 a=b; b=1;
编译器在重排序时会遵守数据依赖性。不会对存在数据依赖性的两个操作之间进行重排序。

重排序

现代的处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操做重排序。
在执行程序时为提高性能,编译器和处理器常常对程序和指令做重排序。

1、编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2、 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3、内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

内存屏障

内存屏障(memory barries)。内存屏障,也称内存栅栏内存栅障屏障指令等, 是一类同步屏障指令,使得CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读/写操作都执行后才可以开始执行此点之后的操作语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。JMM将内存屏障分为4类:
Load Load Barrier: 确保Load Load屏障之前数据的读取 先于 该屏障后数据的读取。
Load Stroe Barrier:确保Load Store屏障之前数据的装载 先于 该屏障之后数据的写入。
Store Store Barrier:确保Store Store屏障之前的数据的写入 先于 屏障之后数据的写入。(缓冲区内应该会将写入效果合并再刷新到主内存)
Store Load Barrier: 确保Store Load 屏障之前数据的写入 先于 屏障之后数据的读取。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。
StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer fully flush)。

Happens-before语义

从JDK5开始,java使用新的JSR -133内存模型(本文除非特别说明,针对的都是JSR- 133内存模型)。JSR-133提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。与程序员密切相关的happens-before规则如下:
1、程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
2、监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
3、volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
4、传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。
注意,两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。举个例子来说吧,
<span style="font-size:12px;">double pi = 3.14;            //A
double r = 1.0;              //B
double area = pi * r * r;    //C</span>
程序顺序规则来看,A 和B都happens-beforeC 。所以顺序为A ->B->C 。且A 与C 、B与C之前存在数据依赖性。所以A->C 、B->C之间是不允许重排序的。A同样happens-before于B 。但是实际执行时A与B顺序是可能会重排的。这就要说到 as-if-serial语义。

as-if-serial语义

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。
as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。

以上是我对 深入理解java内存模型学习后的一点总结。主要参考了 程晓明<<深入理解java内存模型>> ,文中配图也源于该博客,想要深入了解的可以看看啊,个人觉得很值得好好的研究一下。





猜你喜欢

转载自blog.csdn.net/quanzhongzhao/article/details/45619135