Java多线程高并发基础篇(六)-JMM重排序规则

我们知道,重排序的目的是在不改变程序执行结果的前提下,提高编译器和处理器对程序的执行性能。但是,重排序不是任意的,所谓无规矩不成方圆。理解重排序就需要知道重排序必须遵守的规则,总结起来就是我们今天要说的Happens-Before规则。在JSR-133: JavaTM Memory Model and Thread Specification中有相关描述,原版英文请见pdf文件,下载了一份供大家学习。

一. Happens-Before规则

Happens-Before规则规定了哪些情况下指令不能进行重排序。

  • 程序顺序原则:一个线程内,代码执行的过程必须保证语义的串行性( as-if-serial,看起来是串行的;另外如果程序内数据存在依赖,也不允许进行重排序 )。
  • 监视器锁规则:解锁unlock必然发生在加锁lock前。
  • 传递性规则:如果操作A先于操作B,操作B先于操作C,那么操作A必先于操作C。
  • volatile规则:一个共享变量的写操作,必须先于读操作,这是volatile可见性语义的要求。
  • 线程的start规则:线程的start操作先于线程内其他任何操作。
  • 线程的join规则:如果线程ThreadA中执行了ThreadB.join()方法,那么ThreadB的所有操作先于ThreadA中ThreadB.join()返回后的操作。

二. 对于Happens-Before规则解释

1. as-if-serial语义

看起来像串行的--编译器和处理器对重排序的机制对程序员是透明的,但是我们观察到的结果跟按照编写程序的顺序是一致的,这就是看起来像的含义。

举一例说明:

 

int a = 2; // A
int b = 3; // B
int c = a*b; // C

在上述程序中,步骤C依赖于步骤A和B,但是步骤A和B之间没有依赖关系,依赖关系图长这样:

 

根据程序的顺序执行规则,由于C依赖于A和B,那么C的执行顺序不能排在A和B之前,但是A和B的顺序是可以互换的,也就是说,我们按照程序顺序执行的语义,看到的执行顺序是这样:A-->B-->C,但是编译器和处理器可能进行重排序成这样子:B-->A-->C,但是这个过程对我们来说是透明的,但是最终结果跟我们想要的是一样的。这就是看起来像 as-if-serial的语义。

 

2.锁规则

在并发编程中,锁保证了临界区的互斥访问,同时还可以让释放锁的线程向另一个线程发送消息。

我们举一例,先来段代码。

public class MonitorDemo {
  int a = 0;
  public synchronized void writer() { // 1
     a++; // 2
  } // 3
  public synchronized void reader() { // 4
      int i = a; // 5
      ……
  } // 6
}

 

比如现在有两个线程A和B,线程A执行writer方法,线程B随后执行reader方法,根据happens-before原则,我们来梳理下这个过程包含的happens-before关系。

①依据程序顺序执行顺序原则,1-->2-->3;4-->5-->6

②根据监视器锁规则,锁的获取先于锁的释放,那么在A线程未执行完writer时,线程B是无法得到锁的。因此3-->4.

③根据传递性规则,那我们可以得到2-->5.

最后我们得到的happens-before关系图是这样子的:


 

 

 

 这也就是说,当线程B获取到线程A释放的锁后,线程A操作过的共享变量的内容对B是可见的(线程A的步骤2改变了a的值,线程B的步骤5获得了同一把锁后立刻可以得到a的最新值)。

这里我们也对在并发编程中锁的语义进行总结:

  • 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。

  • 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前共享变量所做修改的)消息。

  • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。  

 3.volatile规则

3.1 volatile语义

在并发编程中,单个volatile变量的读、写可以看成是使用同一把锁对单个变量读写操作进行了同步锁操作。

例如,线程A和线程B执行下列代码,线程A执行set()方法,线程B随后执行get()方法。使用volatile
变量和对普通变量进行操作加锁的执行效果是一致的。

下面两个代码执行效果是等价的:

 

public class VolatileDemo {
  volatile long vl = 0L; // 使用volatile声明64位的long型变量
  public void set(long l) {
     vl = l; //1. 单个volatile变量的写
  } 
  public void getAndIncrement () {
     vl++; // 复合(多个)volatile变量的读/写
  }
  public long get() {
     return vl; //2. 单个volatile变量的读
  }
}

public class VolatileDemo2 {
  long vl = 0L; // 64位的long型普通变量
  public synchronized void set(long l) { // 对单个的普通变量的写加同步锁
     vl = l;
  }
  public void getAndIncrement () { // 普通方法调用
     long temp = get(); // 调用已同步的读方法
     temp += 1L; // 普通写操作
     set(temp); // 调用已同步的写方法
  }
  public synchronized long get() { // 对单个的普通变量的读加同步锁
     return vl;
  }
}
换句话说,volatile变量的写与锁的获取有相同的内存语义,volatile变量的读与锁的释放有相同的内存语义,这也就证明了对单个volatile变量的读写操作是原子性的,但是对volatile变量进行复合操作不具有原子性的,这个一定要注意。

 

我们来梳理下使用volatile变量的happens-before关系图,可能对理解更有帮助。

来个例子,线程A和线程B执行下列代码,线程A执行set()方法,线程B随后执行get()方法:

 

public class VolatileDemo {
  volatile long vl = 0L; // 使用volatile声明64位的long型变量
  public void set(long l) {//1
     vl = l; // 2
  } 
  public long get() {//3
     return vl; // 4
  }
}
 

①根据程序顺序执行原则,1-->2,;3-->4

②根据volatile规则,volatile变量的写先于读,所以2-->3

③根据传递性规则,1-->4

所以,我们最后得到的happens-before关系图是这样的:


总结一下,volatile的内存语义:

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

3.2 volatile语义的实现

我们先来看看编译器制定的volatile重排序规则表。


 在规则表中,我们可以明确看到,

当第一个操作是volatile读时,不管第二个操作是什么都不能重排序。

当第一个操作是volatile写时,第二个操作为volatile读、写时不能重排序。

 

为了实现volatile的语义,编译器在编译代码时候,会生成对应的内存屏障指令,来禁止特定类型操作的处理器重排序。JMM采用保守(认为每个都必须这么做)的内存屏障插入策略来实现volatile语义:

  • 在每个volatile操作的前面插入一个StoreStore屏障。
  • 在每个volatile操作的后面插入一个StoreLoad屏障。
  • 在每个volatile操作的后面插入一个LoadLoad屏障。
  • 在每个volatile操作的后面插入一个LoadStore屏障。

举一例子体会下:

public class VolatileBarrierDemo {
	int c;
	volatile int a = 1;
	volatile int b = 2;
	void readAndWrite() {
		int i = a; // 第一个volatile读
		int j = b; // 第二个volatile读
		c = i + j; // 普通写
		a = i + 1; // 第一个volatile写
		b = j * 2; // 第二个 volatile写
	}
}

 最后生成的指令执行示意图如下(红色部分的屏障可以省略掉,因为紧跟着的操作跨越不了已有的屏障):



 

猜你喜欢

转载自zhaodengfeng1989.iteye.com/blog/2419692
今日推荐