Java多线程之内存模型 (2)

对long和double的特殊操作
在一些32位的处理器上,如果要求对64位的long和double的写具有原子性,会有较大的开销,为了照固这种情况,
java语言规范鼓励但不要求虚拟机对64位的long和double型变量的写操作具有原子性,当JVM在这种处理器上运行时,
可能会把64位的long和double拆分成两次32位的写

指令屏障

为了保证内存的可见性,JMM的编译器会禁止特定类型的编译器重新排序;对于处理器的重新排序,
JMM会要求编译器在生成指令序列时插入特定类型的的内存屏障指令,通过内存屏障指令巾纸特定类型的处理器重新排序

JMM规定了四种内存屏障,具体如下:

屏障类型 指令示例 说明
LoadLoad Barriers   Load1;LoadLoad;Load2   确保Load1的数据先于Load2以及所有后续装在指令的装载  
StoreStore Barries   Store1;StoreStore;Store2   确保Store1数据对于其他处理器可见(刷新到内存)先于Store2及后续存储指令的存储  
LoadStore Barriers   Load1;LoadStore;Store2   确保Load1的装载先于Store2及后续所有的存储指令  
StoreLoad Barrier   Store1;StoreLoad;Load2   确保Store1的存储指令先于Load1以及后续所所有的加载指令  

StoreLoad是一个“万能”的内存屏障,他同时具有其他三个内存屏障的效果,现代的处理器大都支持该屏障(其他的内存屏障不一定支持),
但是执行这个内存屏障的开销很昂贵,因为需要将处理器缓冲区所有的数据刷回内存中。

happens-before规则

在JSR-133种内存模型种引入了happens-before规则来阐述操作之间的内存可见性。在JVM种如果一个操作的结果过需要对另一个操作可见,
那么两个操作之间必然要存在happens-bsfore关系:

程序顺序规则:一个线程中的个每个操作,happens-before于该线程的后续所有操作

监视器锁规则:对于一个锁的解锁,happens-before于随后对于这个锁的加锁

volatitle变量规则:对于一个volatile的写,happens-before于认意后续对这个volatile域的读

传递性:如果A happens-before B B happends-beforeC,那么A happends-before C

指令重排序 从源程序到字节指令的重排序

众所周知,JVM执行的是字节码,Java源代码需要先编译成字节码程序才能在Java虚拟机中运行,但是考虑下面的程序;

int a = 1; int b = 1;

在这段代码中,a和b没有任何的相互依赖关系,因此完全可以先对b初始化赋值,再对a变量初始化赋值;

事实上,为了提高性能,编译器和处理器通常会对指令做重新排序。重排序分为3种:

编译器优化的重排序。编译器在不改变单线程的程序语义的前提下,可以安排字语句的执行顺序。编译器的对象是语句,不是字节码,
但是反应的结果就是编译后的字节码和写的语句顺序不一致。

执行级并行的重排序。现代处理器采用了并行技术,来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

内存系统的重排序,由于处理器使用了缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

数据依赖性:如果两个操作访问同一个变量,且两个操作有一个是写操作,则这两个操作存在数据依赖性,改变这两个操作的执行顺序,就会改变执行结果。

尽管指令重排序会提高代码的执行效率,但是却为多线程编程带来了问题,多线程操作共享变量需要一定程度上遵循代码的编写顺序,
也需要将修改的共享数据存储到共享内存中,不按照代码顺序执行可能会导致多线程程序出现内存可见性的问题,那又如何实现呢?

as-if-serial语义

as-if-serial语义:不论程序怎样进行重排序,(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须支持as-if-serial语义。

程序顺序规则

假设存在以下happens-before程序规则:

1) A happens-before B 2) B happens-before C 3) A happens-before C

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwszww.html