CPU缓存模型及并发问题

CPU缓存模型

很简单,现代的计算机技术,内存的读写速度没什么突破,cpu如果要频繁的读写主内存的话,会导致性能较差,计算性能就会低,这样的不适应现代计算机技术的发展。因此就换了一种玩法,在CPU中增加了多层缓存,cpu可以直接操作自己对应的告诉缓存,不需要直接频繁的跟主内存通信,这个是现代计算机技术的一个进步,这样可以保证cpu的计算的效率非常的高
image.png

CPU缓存模型的并发问题

  1. public class VolatileDemo {
  2. //static volatile int flag = 0;
  3. static int flag = 0;
  4. public static void main(String[] args) {
  5. //线程1
  6. new Thread() {
  7. public void run() {
  8. int localFlag = flag;
  9. while(true) {
  10. if(localFlag != flag) {
  11. System.out.println("读取到了修改后的标志位:" + flag);
  12. localFlag = flag;
  13. }
  14. }
  15. };
  16. }.start();
  17. //线程2
  18. new Thread() {
  19. public void run() {
  20. int localFlag = flag;
  21. while(true) {
  22. System.out.println("标志位被修改为了:" + ++localFlag);
  23. flag = localFlag;
  24. try {
  25. TimeUnit.SECONDS.sleep(2);
  26. } catch (Exception e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. };
  31. }.start();
  32. }
  33. }

上面的代码逻辑为,当线程2修改了这个标记flag之后线程1的while循环里面的判断就成立了,就开始打印修改后的标志位值了。但是从执行结果上看并没有按照那个逻辑来执行。
image.png
原因分析:线程0首先启动加载这个flag值,这个值一直是1。随后线程1开始累加值,可以看到flag都加到7了主内存还没有改变,同时线程0的cpu缓存中的值仍然是1,因此线程0的while循环中的判断永远都进不了。
就是因为这个flag的值没有被及时的刷回主内存和线程0的缓存中,自始至终线程0都是拿着最原始的flag来做判断。
image.png

java内存模型及并发问题

java内存模型

虚拟机规范中试图定义一种java内存模型用于屏蔽掉各种硬件和操作系统的内存访问差异,实现让Java程序在各种平台下都能达到一致的内存访问效果。java内存模型和java内存区域不是同一个层次的内存划分,实际上是没有任何联系的,如果一定要有一些联系,那么主内存应该就是堆,工作内存就是线程的私有栈。没有明确定义
看下面的代码,代码主要做了一件事就是开启了两个线程同时调用了HelloWorld的increment方法。接下来用内存模型来进行分析这段代码是如何执行的。
在运行的时候分主内存和工作内存,data这个变量是实例变量就是被加载在堆中也就是主内存中的,当线程获取它的时候需要做以下几步,1.read出来,2.将变量load进线程自己的工作内存中,3.use进线程1进行操作,4.计算完成后assign回工作内存 5.store进一块内存 6.从这块内存中write回主内存
image.pngimage.png

并发问题

线程1把flag修改成1了,但是线程2里面的flag值仍然是0,这时线程2要是拿这个值做一些操作肯定就不对了,就出现多线程并发的问题了。
image.png

内存模型的可见性,原子性,有序性

可见性

image.png
什么是不可见:现在有一个线程对data做修改,另一个线程是要用这个data值去做判断的。在没有任何外界任何关键字干预的情况下,是下图这种执行方式。Thread1和Thread2同时读取了data此时自己的工作内存中的data都是0。随后Thread1将data变成1之后刷入了工作内存和主内存中,这个时候Thread2在一段时间的时候读到的还是data=0,Thread2就一直在while循环。
image.png
具备可见性:当Thread1修改完成后,强制让Thread2去回主内存中读到最新的data值。
image.png

原子性

有原子性:Thread1在做一系列的操作并刷入主存的时候,别的线程不能去做对这个data数据写的操作。只有当Thread1走完了另一个线程才能去对data修改,保证对这个数据操作是正确的。
image.png
没有原子性:两个线程同时拿到data=0的数据,同时修改后又刷入data变量中,Thread1更改完是1刷入主内存,Thread2更改完也是1也刷入了主内存,这个时候就和预期的值不符了。

有序性

编译器和指令器,有的时候为了提高代码执行效率,会将指令重排序。
下面代码重排序之后,让flag = true先执行了,会导致线程2直接跳过while等待,执行某段代码,结果prepare()方法还没执行,资源还没准备好呢,此时就会导致代码逻辑出现异常。
image.png

volatile关键字

用于保证可见性和有序性。

如何保证可见性

没有可见性的时候就如下图一样
image.png
加了volatile关键字,当Thread1刷入主存中,同时把在别的线程工作内存中的那一份data要失效掉,随后Thread2要去读这个值的时候就发现之前读的data就失效了,就要强制从主存中重新去读这个data变量值。
image.png
扩展:在很多的开源中间件系统的源码里,大量的使用了volatile,每一个开源中间件系统,或者是大数据系统,多线程并发。
在多线程并发的情况下,同一个变量有些线程需要更新,有的线程去读,这个时候就应该去使用volatile去修饰该变量。

保证有序性的happens-before规则

编译器、指令器可能对代码重排序,乱排,要遵循一定的规则,happens-before原则。这个原则制定了在一些特殊情况下,不允许编译器、指令器对你写的代码进行指令重排,必须保证你的代码的有序性。
内存模型及相关知识 - 图14

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作,比如说在代码里有先对一个lock.lock(),lock.unlock(),lock.lock()
  3. volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作,volatile变量写,再是读,必须保证是先写,再读
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作,thread.start(),thread.interrupt()
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

volatile的要求是volatile前面的代码一定不能指令重排到volatile变量后面,volatile后面的代码也不能指令重排到volatile前面。

volatile为什么不能保证原子性

image.png
主内存的值都被加载到线程1,2的工作内存中并且都读到这个值并开始运算,随后线程1计算完成后assign回工作内存,同时,线程2的工作内存的值被过期掉。但是有个问题,线程2已经拿着之前工作内存的东西在计算了,此时线程2计算完成后还是可以去把这个值刷回工作内存再刷回主内存中,就把之前的值覆盖了,这明显就不是原子性了。(保证原子性就是说别的线程读这个变量的时候发现已经有线程在读这块数据了那么当前线程就不能读了)

volatile底层如何保证可见性和有序性

1、lock指令:volatile保证可见性。

对这种变量执行写store操作的时,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行检查,自己本地缓存中的数据是否被别人修改。
如果发现别的线程修改了某个缓存的数据,那么CPU就会将这个当前Thread的工作内存中的数据过期掉。随后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据了
lock前缀指令 + MESI缓存一致性协议保证

2、内存屏障:volatile禁止指令重排序

LoadLoad屏障:

  1. Load1
  2. int localVar = this.variable
  3. LoadLoad屏障
  4. Load2
  5. int localVar = this.variable2

Load1;LoadLoad;Load2,确保Load1数据的装载先于Load2后所有装载指令,他的意思,Load1对应的代码和Load2对应的代码,是不能指令重排的

StoreStore屏障:

Store1:
this.variable = 1
StoreStore屏障
Store2:
this.variable2 = 2

Store1;StoreStore;Store2,确保Store1的数据一定刷回主存,对其他cpu可见,先于Store2以及后续指令

LoadStore屏障:Load1;LoadStore;Store2,确保Load1指令的数据装载,先于Store2以及后续指令

StoreLoad屏障:Store1;StoreLoad;Load2,确保Store1指令的数据一定刷回主存,对其他cpu可见,先于Load2以及后续指令的数据装载

总结

对于volatile修饰的变量,这个变量在程序中读/写的操作都会加入内存屏障, 编译器根据这些内存屏障去判断什么可以重排序什么不可以.

volatile variable = 1 this.variable = 2 => store操作 int localVariable = this.variable => load操作

每个volatile写操作前面,加StoreStore屏障,禁止上面的普通写和他重排;
每个volatile写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排
每个volatile读操作后面,加LoadLoad屏障,禁止下面的普通读和voaltile读重排;
每个volatile读操作后面,加LoadStore屏障,禁止下面的普通写和volatile读重排
这些屏障加的方式根据底层硬件实现,不一定是上面这些组合方式,但是一定是通过内存屏障来实现的。

java中的原子性

java规范所有变量的赋值操作都是原子性的

int i = 0; resource = loadedResources; 各种变量的简单赋值操作都是原子性的. 同时,引用类型的变量的赋值写操作也是原子的.

什么不是简单赋值: i++ 这种不是简单复制,这个操作是要去加载i在计算中间过程相对赋值.

特例:在32位虚拟机中long和double变量的写操作不是原子性的.
long和double是64位的,long/double i = 30 并发的时候就会有线程在变量的高32位修改,有的在低32位修改,就有可能在赋值的时候这个i变成乱码-3333344429.因为高低32位赋值错了,导致二进制转换的时候是一个奇怪的数字

这个时候就去用volatile去保证在32位jvm中对long/double的赋值写去保证原子性

硬件级别分析可见性

简要分析可见性

可见性问题

下图中一些写流程存在一些特殊情况后面再说,大致流程图中表达的是没问题的。
1.有可能在寄存器的级别,导致变量副本的更新,无法让其他处理器看到。从图中能看出,一个处理器(cpu)包含寄存器、高速缓存、写缓冲器,这三个东西在处理器1的角度上是看不到处理器0中这些组件的数据。
2.然后一个处理器运行的线程对变量的写操作都是针对写缓冲来的(store buffer)并不是直接更新主内存,所以很可能导致一个线程更新了变量,但是仅仅是在写缓冲区里罢了,没有更新到主内存里去。(这是一种“写回”的策略,为了降低总线事务的数量因此就采用“写回”。 写情况比较复杂这里简单说明:写有两种方式 直写/写回 直写就是将高速缓存中的值直接修改后写回到内存中,缺点就是每条存储指令都会引起总线上的一个写事务。 写回就是推迟内存的更新,只有当替换算法要要驱逐已更新的块时,才将新值更新到内存中。ps:处理器中数据交互离不开总线接口,通过这个做数据交互。计算机中其他模块也是通过数据总线DB(Data Bus)、地址总线AB(Address Bus)和控制总线CB(Control Bus)做不同的交互)
3.然后即使这个时候一个处理器的线程更新了写缓冲区之后,将更新同步到了自己的高速缓存里(cache,或者是主内存),然后还把这个更新通知给了其他的处理器,但是其他处理器可能就是把这个更新放到无效队列里去,没有更新他的高速缓存。
image.png

MESI协议解决可见性问题

根据具体底层硬件的不同,MESI协议的实现是有区别的。
MESI协议有一种实现,就是一个处理器将另外一个处理器的高速缓存中的更新后的数据拿到自己的高速缓存中来更新一下,这样所有处理器的缓存实现同步了,然后各个处理器的线程看到的数据就一样了
1.flush指令刷新处理器缓存,把自己更新的值刷新到高速缓存里去(或者是主内存),因为必须要刷到高速缓存(或者是主内存)里,才有可能在后续通过一些特殊的机制让其他的处理器从自己的高速缓存(或者是主内存)里读取到更新的值。除了flush以外,还会发送一个消息到总线(bus),通知其他处理器,某个变量的值被当前处理器给修改了
2.refresh指令更新处理器缓存,处理器中的线程在读取一个变量的值的时候,如果发现其他处理器的线程更新了变量的值,必须从其他处理器的高速缓存(或者是主内存)里,读取这个最新的值,更新到自己的高速缓存中。
总结:flush是强制刷新数据到高速缓存(主内存),不要仅仅停留在写缓冲器里面;refresh,是从总线嗅探发现某个变量被修改,必须强制从其他处理器的高速缓存(或者主内存)加载变量的最新值到自己的高速缓存里去。内存屏障的使用,在底层硬件级别的原理,其实就是在执行flush和refresh。对一个变量加了volatile修饰之后,对这个变量的写操作,会执行flush处理器缓存,把数据刷到高速缓存(或者是主内存)中,然后对这个变量的读操作,会执行refresh处理器缓存,从其他处理器的高速缓存(或者是主内存)中,读取最新的值。

有序性

指令重排的层次

代码在实际执行的时候那个顺序可能在很多环节都会被人给重排序,一旦重排序之后,在多线程并发的场景下,就有可能会出现一些问题
1.编译后的代码的执行顺序:java里有两种编译器,一个是静态编译器(javac),一个是动态编译器(JIT)。javac负责把.java文件中的源代码编译为.class文件中的字节码,这个一般是程序写好之后进行编译的。JIT负责把.class文件中的字节码编译为JVM所在操作系统支持的机器码,一般在程序运行过程中进行编译。在这个过程中编译器有可能调整代码的执行顺序,JIT对指令重排是比较多的。
2.处理器的执行顺序:哪怕你给处理器一个代码的执行顺序,但是处理器还是可能会重排代码,更换一种执行顺序,JIT编译好的指令的时候,还是可能会调整顺序
3.内存重排序:有可能这个处理器在实际执行指令的过程中,在高速缓存和写缓冲器、无效队列等等,硬件层面的组件,也可能会导致指令的执行看起来的顺序跟想象的不太一样
image.png

JIT指令重排

什么是指令重排

public class MyObject {

    private Resource resource;

    public MyObject() {
        this.resource = loadResource(); // 从配置文件里加载数据构造Resource对象
    }

    public void execute() {
        this.resource.execute();
    }

}

// 线程1:

MyObject myObj = new MyObject(); => 这个是我们自己写的一行代码

// 线程2:

myObj.execute();

// 步骤1:以MyObject类作为原型,给他的对象实例分配一块内存空间,objRef就是指向了分配好的内存空间的地址的引用,指针

objRef = allocate(MyObject.class);

// 步骤2:就是针对分配好内存空间的一个对象实例,执行他的构造函数,对这个对象实例进行初始化的操作,执行我们自己写的构造函数里的一些代码,对各个实例变量赋值,初始化的逻辑

invokeConstructor(objRef);

// 步骤3:上两个步骤搞定之后,一个对象实例就搞定了,此时就是把objRef指针指向的内存地址,赋值给我们自己的引用类型的变量,myObj就可以作为一个类似指针的概念指向了MyObject对象实例的内存地址

myObj = objRef;

在这一段有三个步骤
1.以MyObject类作为原型,给他的对象实例分配一块内存空间,objRef就是指向了分配好的内存空间的地址的引用,指针objRef = allocate(MyObject.class);
2.就是针对分配好内存空间的一个对象实例,执行他的构造函数,对这个对象实例进行初始化的操作,执行我们自己写的构造函数里的一些代码,对各个实例变量赋值,初始化的逻辑invokeConstructor(objRef);
3.上两个步骤搞定之后,一个对象实例就搞定了,此时就是把objRef指针指向的内存地址,赋值给我们自己的引用类型的变量,myObj就可以作为一个类似指针的概念指向了MyObject对象实例的内存地址myObj = objRef;
JIT动态编译为了加速代码的执行速度,步骤2的过程可能是比较耗时,比如在里面执行一些网络通信,磁盘文件读写。为了提高效率JIT 可能会把这个过程变成1->3->2

并发出现的问题场景:
线程1:执行了1,3此时没有执行第二步,此时myObject虽然不是null但是由于没有初始化完毕,里面的那个resource引用对象仍然是个null值。
线程2:在别的地方执行了resource.execute(),那么此时resource是null,这个时候就有问题了。

因此java肯定是不允许这种情况发生的,java自身就会有一套规定在jvm执行的时候保证某些情况的代码就不允许指令重排。

volatile语义禁止指令重排之单例double check

public class SingletonClass { 
  private volatile static SingletonClass instance = null; 
  private TestObject testObject;
  public static SingletonClass getInstance() { 
    if (instance == null) { //操作1
      synchronized (SingletonClass.class) { 
        if(instance == null) {  //操作2
            //这里再判断一下就是防止两个线程都走过了操作1,然后其中一个线程抢到锁了,但是另外一个没有,下面就还得判断一下。
          instance = new SingletonClass(); //操作3
        } 
      } 
    } 
    return instance;  //操作4
  } 
  private SingletonClass() { 
      this.testObject = new TestObject();
  } 
}

instance被赋值的时候要走以下三个步骤
1. 分配一块内存。
2. 在内存上初始化成员变量,初始化该类。
3. 把instance引用指向内存。
没有volatile修饰instance,由于初始化该类的时候还有一个testObject要初始化。JIT编译器就有可能优化代码,导致执行顺序是1,3,2。当下一个线程来了,就开始走“操作1”判断了,当前线程一看,instance都指向一片内存了不是指向null了,判断就不成立了,随后就跳到“操作4”,此时instance = new SingletonClass();还没初始化完,返回结果就是个null。
加入这个volatile第一个目的就是让别的cpu感知到instance的变化,同时我们知道volatile是遵循happens-before原则的,同时happens-before是有传递性的。线程A对volatile变量的一些 操作状态/过程 在底层有一定操作(例如MESI)是能使线程B感知到的。因此线程A执行操作3的时候,volatile保证instance赋值操作不被打乱,同时也保证了下一个线程做“操作1”这个判断的时候发生在“操作3”后面。

处理器执行指令的乱序

指令被发送到处理中不是立即去执行的,也许有的指令是需要进行网络通信、磁盘读写、获取锁。为了提升效率,现代处理器里面走的都是乱序执行机制。
也就是说所有指令进入处理器,哪一个指令就绪了就可以先执行,不是按照代码顺序来的。执行完后会将这些指令放入重排序处理器中,随后把这些指令放入高速缓冲器或者写缓区中。
这就导致处理器可能压根儿就是乱序在执行我们代码编译后的指令
image.png

高速缓存和写缓冲器的内存指令重排

这种情况是没有加内存屏障,单纯就是指令重排。
虽然在代码层面甚至在字节码层面,都做了防止编译器对代码重排的操作。但是形成指令发过来后,在具体内存层面对Load和Store这些操作有可能去被优化掉。(这种情况是在处理完成后,当指令有写操作的时候容易有问题,万一读在写的前面就会有问题)
(1)LoadLoad重排序:一个处理器先执行一个L1读操作,再执行一个L2读操作;但是另外一个处理器看到的是先L2再L1
(2)StoreStore重排序:一个处理器先执行一个W1写操作,再执行一个W2写操作;但是另外一个处理器看到的是先W2再W1
(3)LoadStore重排序:一个处理器先执行一个L1读操作,再执行一个W2写操作;但是另外一个处理器看到的是先W2再L1
(3)StoreLoad重排序:一个处理器先执行一个W1写操作,再执行一个L2读操作;但是另外一个处理器看到的是先L2再W1
写缓冲器为了提升性能,有可能先后到来W1和W2操作了之后,他先执行了W2操作,再执行了W1操作。那这个时候其他处理器看到的可不就是先W2再W1了,这就是StoreStore重排序

内存屏障在硬件上的表现

高速缓存数据结构

底层数据结构是一个拉链散列表,里面存在许多bucket,每个bucket上挂着一个cache entry,每个entry由tag、cache line和flag组成。tag指向了缓存数据在内存中的位置,cache line是缓存的数据包含多个变量的值,flag标识了缓存行的状态。
处理器在读写高速缓存的时候,实际上会根据变量名执行一个内存地址解码的操作,解析出来3个东西,index、tag和offset。index用于定位到拉链散列表中的某个bucket,tag是用于定位cache entry,offset是用于定位一个变量在cache line中的位置。

缓存一致性协议MESI

MESI规定:对一个共享变量的读操作可以多处理器并发执行,但是如果对一个共享变量的写操作,只有一个处理器可以操作 ,实际上也是使用排他锁的一个机制保证同一时间只有一个处理器做写操作。
(1)invalid:无效的,标记为I,这个意思就是当前cache entry无效,里面的数据不能使用
(2)shared:共享的,标记为S,这个意思是当前cache entry有效,而且里面的数据在各个处理器中都有各自的副本,但是这些副本的值跟主内存的值是一样的,各个处理器就是并发的在读而已
(3)exclusive:独占的,标记为E,这个意思就是当前处理器对这个数据独占了,只有他可以有这个副本,其他的处理器都不能包含这个副本
(4)modified:修改过的,标记为M,只能有一个处理器对共享数据更新,所以只有更新数据的处理器的cache entry,才是exclusive状态,表明当前线程更新了这个数据,这个副本的数据跟主内存是不一样的
示例:
1.处理器0要读取某个值,通过总线从内存中读取到高速缓存中,此时将flag改为S
2.当处理器0开始对这个数据做修改的时候,如果当前flag是s,就要发送一个invalidate消息到总线,别的处理器收到这个消息了则将本地该数据副本的flag置为i,并发送invalidate ack消息给处理器0
3.处理器0收到这个消息之后,将flag置为e,独占这条消息。此时别的处理器就不能在对这个消息做修改了,别的处理器要修改的时候也是要发送invalidate消息给处理器0,那么此时处理器0是不会发送ack消息回去的。别的处理器收不到这个消息自然就不能对数据做修改
4.处理器0就是修改这条数据,接着将数据设置为M,也有可能是把数据此时强制写回到主内存中,具体看底层硬件实现。
5.此时其他处理器要读这条信息了,就发现当前的副本状态是I,就要重新发送read消息去别的处理器的高速缓存中取或者从内存直接拿,具体也是看硬件如何去实现了。

image.png

写缓冲器和无效队列优化MESI

MESI在实现过程中有两个地方会影响效率
1.每次写数据的时候都要发送invalidate消息等待所有处理器返回ack,然后获取独占锁后才能写数据,那可能就会导致性能很差了,因为这个对共享变量的写操作,实际上在硬件级别变成串行的了
2.再一个就是在别的处理器收到invalidate消息同时还要更改状态才能返回ack,这样也是比较慢的
因此就引入了写缓冲器和无效队列去解决。
写缓冲器:一个处理器写数据的时候,直接把数据写入缓冲器,同时发送invalidate消息,然后就认为写操作完成了,接着就做事了,不会阻塞在这里。接着这个处理器如果之后收到其他处理器的ack消息之后才会把写缓冲器中的写结果拿出来,通过对cache entry设置为E加独占锁,同时修改数据,然后设置为M。包括查询数据的时候,会先从写缓冲器里查,因为有可能刚修改的值在这里,然后才会从高速缓存里查,这个就是存储优化
无效队列:其他处理器在接收到了invalidate消息之后,不需要立马过期本地缓存,直接把消息放入无效队列,就返回ack给那个写处理器了,这就进一步加速了性能,然后如果处理器要读数据的时候就看一下无效队列有没有对应的消息,有了之后再更新本地缓存。
image.png

MESI的可见性&有序性问题

可见性:MESI只是保证了多处理器并发的问题,一个处理器写操作仍然是写入自己的写缓冲器或者高速缓存,别的处理器中的副本并没有被更新导致可见性的问题。同时别的无效队列也不是立即就把数据置为无效也会导致获取值的时候拿到的是以前的值。
有序性:未加volatile关键字的时候
(1)StoreLoad重排序

int a = 0; int c = 1; 线程1: a = 1; int b = c;

一种假象
a=1 是一种store的操作,有可能线程是将数据写入写缓冲器中的,int b=c c是一个load的操作。一般情况下c直接就读到了,也就是load操作成功了。由于store写入写缓冲器别的处理器是无法知道,过一段时间才被同步到别的处理器或者高速缓存中。外部看这个操作的时候就有可能是load先做了,过了一会store才做。这就是一种读写的一种假象。如果是对同一个数据发生了这种重排操作,就会发生数据不一致的现象。
(2)StoreStore重排序

resource = loadResource(); //store1 loaded = true;//store2

针对两种数据,一种标记为M一种标记为S。针对S的数据由于是共享的,处理器写数据一定是写在写缓冲器中的,对于标记是M的处理器是不是就直接可以在高速缓存中去修改,这样的话这个写操作对外是不是也是不可见了也是得过一会去同步。因此就有可能store2先执行,store1后执行了。发生了重排现象。
image.png

内存屏障解决上述硬件产生的问题

解决可见性问题

Store屏障

当加了store屏障,强制性要求你对一个写操作必须阻塞等待到其他的处理器返回invalidate ack之后,对数据加锁,然后修改数据到高速缓存中,必须在写数据之后,强制执行flush操作。要求修改数据不能在写缓冲器中。

Load屏障

加了Load屏障后,从高速缓存中读取数据的时候,如果发现无效队列中正好有当前数据的invalidate消息,强制把本地的消息给无效掉,重新从内存或者别的处理器的高速缓存中获取数据(refresh操作)。

解决有序性问题

StoreSotre屏障

resource = loadResource(); StoreStore屏障 loaded = true;

会强制让写数据的操作全部按照顺序写入写缓冲器里,他不会让你第一个写到写缓冲器里去,第二个写直接修改高速缓存了,上面这种例子就不会发生外界看着一个先store一个后store。

StoreLoad屏障

a = 1; // 强制要求必须直接写入高速缓存,不能停留在写缓冲器里,清空写缓冲器里的这条数据 int b = c;

会强制先将写缓冲器里的数据写入高速缓存中,接着读数据的时候强制清空无效队列,对里面的validate消息全部过期掉高速缓存中的条目,然后强制从主内存里重新加载数据。