内存重排序

/ Java / 没有评论 / 1084浏览

内存重排序

上一章描述了内存可见性问题、那么这章到了重点 内存重排序、volatile 与 synchronized 为什么能保证线程安全、原子操作

重排序与内存可见性的关系

Store Buffer 的延迟写入是重排序的一种,称为内存重排序(Memory Ordering)。除此之外,还 有编译器和CPU的指令重排序。 重排序类型:

  1. 编译器重排序。 对于没有先后依赖关系的语句,编译器可以重新调整语句的执行顺序。
  2. CPU指令重排序。 在指令级别,让没有依赖关系的多条指令并行。
  3. CPU内存重排序。 CPU有自己的缓存,指令的执行顺序和写入主内存的顺序不完全一致。 在三种重排序中,第三类就是造成“内存可见性”问题的主因,如下案例: 线程1:
    X=1 a=Y 线程2:
    Y=1 b=X

假设X、Y是两个全局变量,初始的时候,X=0,Y=0。请问,这两个线程执行完毕之后,a、b的正 确结果应该是什么? 很显然,线程1和线程2的执行先后顺序是不确定的,可能顺序执行,也可能交叉执行,最终正确的 结果可能是:

  1. a=0,b=1
  2. a=1,b=0
  3. a=1,b=1 也就是不管谁先谁后,执行结果应该是这三种场景中的一种。但实际可能是a=0,b=0。 两个线程的指令都没有重排序,执行顺序就是代码的顺序,但仍然可能出现a=0,b=0。原因是线程 1先执行 X=1,后执行 a=Y,但此时 X=1 还在自己的 Store Buffer 里面,没有及时写入主内存中。所以,线 程2看到的X还是0。线程2的道理与此相同。 虽然线程1觉得自己是按代码顺序正常执行的,但在线程2看来,a=Y和X=1顺序却是颠倒的。指令没 有重排序,是写入内存的操作被延迟了,也就是内存被重排序了,这就造成内存可见性问题。

内存屏障

为了禁止编译器重排序和 CPU 重排序,在编译器和 CPU 层面都有对应的指令,也就是内存屏障 (Memory Barrier)。这也正是JMM和happen-before规则的底层实现原理。 编译器的内存屏障,只是为了告诉编译器不要对指令进行重排序。当编译完成之后,这种内存屏障 就消失了,CPU并不会感知到编译器中内存屏障的存在。 而CPU的内存屏障是CPU提供的指令,可以由开发者显示调用。 内存屏障是很底层的概念,对于 Java 开发者来说,一般用 volatile 关键字就足够了。但从JDK 8开 始,Java在Unsafe类中提供了三个内存屏障函数,如下所示。

public final class Unsafe { // ... public native void loadFence(); public native void storeFence(); public native void fullFence(); // ... }

在理论层面,可以把基本的CPU内存屏障分成四种:

  1. LoadLoad:禁止读和读的重排序。
  2. StoreStore:禁止写和写的重排序。
  3. LoadStore:禁止读和写的重排序。
  4. StoreLoad:禁止写和读的重排序。 Unsafe中的方法:
  5. loadFence=LoadLoad+LoadStore
  6. storeFence=StoreStore+LoadStore
  7. fullFence=loadFence+storeFence+StoreLoad

as-if-serial语义

重排序的原则是什么?什么场景下可以重排序,什么场景下不能重排序呢?

1.单线程程序的重排序规则

无论什么语言,站在编译器和CPU的角度来说,不管怎么重排序,单线程程序的执行结果不能改 变,这就是单线程程序的重排序规则。 即只要操作之间没有数据依赖性,编译器和CPU都可以任意重排序,因为执行结果不会改变,代码 看起来就像是完全串行地一行行从头执行到尾,这也就是as-if-serial语义。 对于单线程程序来说,编译器和CPU可能做了重排序,但开发者感知不到,也不存在内存可见性问 题。

2.多线程程序的重排序规则

编译器和CPU的这一行为对于单线程程序没有影响,但对多线程程序却有影响。 对于多线程程序来说,线程之间的数据依赖性太复杂,编译器和CPU没有办法完全理解这种依赖性 并据此做出最合理的优化。 编译器和CPU只能保证每个线程的as-if-serial语义。 线程之间的数据依赖和相互影响,需要编译器和CPU的上层来确定。 上层要告知编译器和CPU在多线程场景下什么时候可以重排序,什么时候不能重排序。

happen-before

使用happen-before描述两个操作之间的内存可见性。 public final class Unsafe { // ... public native void loadFence(); public native void storeFence(); public native void fullFence(); // ... } 1234567 java内存模型(JMM)是一套规范,在多线程中,一方面,要让编译器和CPU可以灵活地重排序; 另一方面,要对开发者做一些承诺,明确告知开发者不需要感知什么样的重排序,需要感知什么样的重 排序。然后,根据需要决定这种重排序对程序是否有影响。如果有影响,就需要开发者显示地通过 volatile、synchronized等线程同步机制来禁止重排序。 关于happen-before: 如果A happen-before B,意味着A的执行结果必须对B可见,也就是保证跨线程的内存可见性。A happen before B不代表A一定在B之前执行。因为,对于多线程程序而言,两个操作的执行顺序是不确 定的。happen-before只确保如果A在B之前执行,则A的执行结果必须对B可见。定义了内存可见性的约 束,也就定义了一系列重排序的约束。 基于happen-before的这种描述方法,JMM对开发者做出了一系列承诺:

  1. 单线程中的每个操作,happen-before 对应该线程中任意后续操作(也就是 as-if-serial语义保 证)。
  2. 对volatile变量的写入,happen-before对应后续对这个变量的读取。
  3. 对synchronized的解锁,happen-before对应后续对这个锁的加锁。 …… JMM对编译器和CPU 来说,volatile 变量不能重排序;非 volatile 变量可以任意重排序。

happen-before的传递性

除了这些基本的happen-before规则,happen-before还具有传递性,即若A happen-before B,B happen-before C,则A happen-before C。 如果一个变量不是volatile变量,当一个线程读取、一个线程写入时可能有问题。那岂不是说,在多 线程程序中,我们要么加锁,要么必须把所有变量都声明为volatile变量?这显然不可能,而这就得归功 于happen-before的传递性。


class A { 
  private int a = 0; 
  private volatile int c = 0;         
  public void set() { 
  a = 5; // 操作1 c = 1; // 操作2 
  }
  
  public int get() {
   int d = c; // 操作3 return a; // 操作4 
  } 
}

假设线程A先调用了set,设置了a=5;之后线程B调用了get,返回值一定是a=5。为什么呢? 操作1和操作2是在同一个线程内存中执行的,操作1 happen-before 操作2,同理,操作3 happenbefore操作4。又因为c是volatile变量,对c的写入happen-before对c的读取,所以操作2 happenbefore操作3。利用happen-before的传递性,就得到: 操作1 happen-before 操作2 happen-before 操作3 happen-before操作4。 所以,操作1的结果,一定对操作4可见。


class A {
  private int a = 0; 
  private int c = 0; 
  public synchronized void set() {
    a = 5; // 操作1 
    c = 1; // 操作2 
  }
  
  public synchronized int get() {
   return a; 
  } 
 }

假设线程A先调用了set,设置了a=5;之后线程B调用了get,返回值也一定是a=5。 因为与volatile一样,synchronized同样具有happen-before语义。展开上面的代码可得到类似于下 面的伪代码:


线程A:
加锁; // 操作1 
  a= 5; // 操作2 
  c = 1; // 操作3 
  解锁; // 操作4 
线程B:
  加锁; // 操作5 
  读取a; // 操作6 
  解锁; // 操作7

根据synchronized的happen-before语义,操作4 happen-before 操作5,再结合传递性,最终就 会得到: 操作1 happen-before 操作2……happen-before 操作7。所以,a、c都不是volatile变量,但仍然有 内存可见性。