并发编程之 指令重排(JMM有序性)

指令重排

在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化

源代码   ->   编译器优化的重排   ->   指令并行的重排   ->   内存系统的重排   ->   最终执行的指令

 

单线程里面,确保程序最终执行结果和代码顺序执行的结果一致

处理器在进行重排序时必须考虑指令间的数据依赖性

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

 

int a,b,x,y = 0;

线程 1 线程2
x = a; y = b;
b = 1; a = 2;
 
x = 0   y = 0

如果编译器对这段程序代码执行重排优化后,可能出现下列情况

线程 1 线程2
b = 1; a = 2;
x = a; y = b;
 
x = 2    y = 1

 

 

volatile 如何禁止指令重排

volatile关键字通过提供"内存屏障"的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

大多数的处理器都支持内存屏障的指令。

对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:

在每个volatile写操作的前面插入一个StoreStore屏障。

在每个volatile写操作的后面插入一个StoreLoad屏障。

在每个volatile读操作的后面插入一个LoadLoad屏障。

在每个volatile读操作的后面插入一个LoadStore屏障。

 

什么是内存屏障  

 

内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。

作用 1.保证特定操作的执行顺序 2.保证某些变量的内存可见性(volatile利用该特性实现内存可见性)

内存屏障可以被分为以下几种类型

LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

 

 

由于编译器和处理器都能执行指令重排的优化,如果在指令间插入一条内存屏障(Momory Barrier)则会告诉编译器和CPU,不管什么命令都不能和这条Memory Barrier指令重新排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化

内存屏障的另一个作用就是强制刷出各种CPU缓存数据,因此任何CPU上的线程都可以读取到这些数据的最新版本。

 

猜你喜欢

转载自www.cnblogs.com/chuzhong/p/12731889.html