6.Store Bufferes引起的指令重排序问题
executeToCPU0(){ a=1; b=1; } executeToCPU1(){ while(b==1){ //b=1 assert(a==1); //a=0 } }
7.通过内存屏障禁止了指令重排序
X86的memory barrier指令包括lfence(读屏障) sfence(写屏障) mfence(全屏障)
Store Memory Barrier(写屏障) ,告诉处理器在写屏障之前的所有已经存储在存储缓存(storebufferes)中的数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对屏障之后的读或者写是可见的
Load Memory Barrier(读屏障) ,处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的
Full Memory Barrier(全屏障) ,确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作
volatile int a=0; executeToCpu0(){ a=1; //storeMemoryBarrier()写屏障,写入到内存 b=1; // CPU层面的重排序 //b=1; //a=1; } executeToCpu1(){ while(b==1){ //true loadMemoryBarrier(); //读屏障 assert(a==1) //false } }
8.java内存模型是如何解决指令重排序 和 缓存问题?
并发编程导致可见性问题的根本原因是缓存及重排序。 而JMM 实际上就是提供了合理的禁用缓存以及禁止重排序的方法。所以它最核心的价值在于解决可见性和有序性。
JMM 抽象模型分为主内存、工作内存(本地内存);
主内存:是所有线程 共享的,一般是实例对象、静态字段、数组对象等存储在 堆内存中的变量。
工作内存(本地内存):是每个线程独占的,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量,线程之间的共享变量值的传递都是基于主内存来完成。
9.JMM 是如何解决可见性有序性问题的?
JMM 提供了一些禁用缓存以及进制重排序的方法,来解决可见性和有序性问题。这些方法大家都很熟悉: volatile、synchronized、final;
10.JMM 如何解决顺序一致性问题?
编译器重排序,JMM 提供了禁止特定类型的编译器重排序。
处理器重排序,JMM 会要求编译器生成指令时,会插入内存屏障来禁止处理器重排序。
11,JMM的内存屏障
1、为什么会有内存屏障?
(1)CPU的高速缓存会缓存主存中的数据,缓存的目的就是为了提高性能,避免每次都要向内存取。但是这样的弊端也很明显:不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。
(2)用volatile关键字修饰变量可以解决上述问题,那么volatile是如何做到这一点的呢?那就是内存屏障。
2、内存屏障是什么?
硬件层的内存屏障分为两种:Load Barrier (读屏障) 和 **Store Barrier(写屏障)**及 Full Barrier(全屏障) 是读屏障和写屏障的合集。
内存屏障有两个作用:
(1)阻止屏障两侧的指令重排序;
(2)写屏障:强制把写缓冲区/高速缓存中的脏数据等写回主内存,读屏障:将缓冲区/高速缓存中相应的数据失效。
3、java内存屏障?
java的内存屏障通常所谓的四种即LoadLoad(LL),StoreStore(SS),LoadStore(LS),StoreLoad(SL)实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。
(1)LoadLoad(LL)屏障:对于这样的语句Load1; (2)LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
(3)StoreStore(SS)屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
(4)LoadStore(LS)屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad(SL)屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
4、volatile语义中的内存屏障?
volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:
(1)在每个volatile写操作前插入StoreStore(SS)屏障,在写操作后插入StoreLoad屏障;
(2)在每个volatile读操作前插入LoadLoad(LL)屏障,在读操作后插入LoadStore屏障;
由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了轻量锁的特性。
5、final语义中的内存屏障?
对于final域,编译器和CPU会遵循两个排序规则:
(1)、新建对象过程中,构造体中对final域的初始化写入和这个对象赋值给其他引用变量,这两个操作不能重排序;
(2)、初次读包含final域的对象引用和读取这个final域,这两个操作不能重排序;(意思就是先赋值引用,再调用final值)
总之上面规则的意思可以这样理解:必需保证一个对象的所有final域被写入完毕后才能引用和读取。这也是内存屏障的起的作用:
1、写final域:在编译器写final域完毕,构造体结束之前,会插入一个StoreStore屏障,保证前面的对final写入对其他线程/CPU可见,并阻止重排序。
2、读final域:在上述规则2中,两步操作不能重排序的机理就是在读final域前插入了LoadLoad屏障。
3、X86处理器中,由于CPU不会对写-写操作进行重排序,所以StoreStore屏障会被省略;而X86也不会对逻辑上有先后依赖关系的操作进行重排序,所以LoadLoad也会变省略。
11.HappenBefore原则
HappenBefore解决的是可见性问题
定义:前一个操作的结果对于后续操作是可见的。在 JMM 中,如果一个操作执行的结果需要对另一个操作课件,那么这两个操作必须要存在 happens-before 关系。这两个操作可以是同一个线程,也可以是不同的线程。
1、as-if-serial 规则(程序顺序执行):单个线程中的代码顺序不管怎么重排序,对于结果来说是不变的。
2、volatile 变量规则,对于 volatile 修饰的变量的写的操作, 一定 happen-before 后续对于 volatile 变量的读操作;
3、监视器锁规则(monitor lock rule):对一个监视器的解锁,happens-before于随后对这个监视器的加锁。
4、传递性规则:如果A happens-before B,且B happens-before C,那么A happens-before C。
5、start 规则:如果线程 A 执行操作 ThreadB.start(),那么线程 A 的 ThreadB.start()操作 happens-before 线程 B 中的任意操作。
6、join 规则:如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功返回。
class VolatileExample { int a = 0; volatile boolean flag = false; public void writer() { a = 1; //1 flag = true; //2 } public void reader() { if (flag) { //3 int i = a; //4 ... } } }
**假设**线程A执行writer()方法之**后**,线程B执行reader()方法,那么线程B执行4的时候一定能看到线程A写入的值吗?注意,a不是volatile变量。 答案是**肯定的**。因为根据happens-before规则,我们可以得到如下关系: 根据**程序顺序规则**,1 happens-before 2;3 happens-before 4。 根据**volatile规则**,2 happens-before 3。 根据**传递性规则**,1 happens-before 4。 因此,综合运用**程序顺序规则**、**volatile规则及传递性规则**,我们可以得到1 happens-before 4,即线程B在执行4的时候一定能看到A写入的值。
3,简单介绍一下volatile关键字的作用 ?
volatile 关键字来保证可见性和禁止指令重排
提供了一种禁止指令重排序的机制和通过内存屏障的机制解决可见性问题。
参考简书:并发编程-(4)-JMM基础(总线锁、缓存锁、MESI缓存一致性协议、CPU 层面的内存屏障)