happens-before原则

starlin 1,102 2018-05-15

随着对java并发的越来越深入,到后面总会接触到happens-before规则。由于存在线程本地内存和主内存的原因,加上重排序,会导致多线程环境下的存在可见性的问题,我们无法保证所有的场景某个线程改的变量对其他线程可见,但是我们可以指定某些规则,这些规则就是happens-before。

概述

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before,happens-before原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题。

下面简单的例子来解释happens-before

i = 1 //线程A执行
j = 1 //线程B执行

j 是否等于1呢?假定线程A的操作(i = 1)happens-before线程B的操作(j = i),那么可以确定线程B执行后j = 1 一定成立,如果他们不存在happens-before原则,那么j = 1 不一定成立。

happens-before原则

在java语言中大概有8大happens-before原则,分别如下:

1. 程序次序规则:

一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序(重排序后面会详细介绍)。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对单线程有效,在多线程环境下无法保证正确性。

示例如下代码:

int a = 3; //①
int b = a + 3; //②

这里的对b的赋值操作会用到变量a,那么java的“单线程happen-before原则”就保证②的中的a的值一定是3,因为① 书写在②前面, ①对变量a的赋值操作对②一定可见。因为② 中有用到①中的变量a,再加上java内存模型提供了“单线程happen-before原则”,所以java虚拟机不许可操作系统对① ② 操作进行指令重排序,即不可能有② 在①之前发生,

但是对于下面的代码:

int a = 3;
int b = 4;

两个语句直接没有依赖关系,所以指令重排序可能发生,即对b的赋值可能先于对a的赋值。

2. 锁定规则:

一个unLock操作先行发生于后面对同一个锁额lock操作;

无论是在单线程环境还是多线程环境,一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。

示例代码如下:

public class A {
   public int var;

   private static A a = new A();

   private A(){}

   public static A getInstance(){
       return a;
   }

   public synchronized void method1(){
       var = 3;
   }

   public synchronized void method2(){
       int b = var;
   }

   public void method3(){
       synchronized(new A()){ //注意这里和method1 method2 用的可不是同一个锁哦
           var = 4;
       }
   }
}

//线程1执行的代码:
A.getInstance().method1();

//线程2执行的代码:
A.getInstance().method2();

//线程3执行的代码:
A.getInstance().method3();

如果某个时刻执行完“线程1” 马上执行“线程2”,因为“线程1”执行A类的method1方法后肯定要释放锁,“线程2”在执行A类的method2方法前要先拿到锁,符合“锁的happen-before原则”,那么在“线程2”method2方法中的变量var一定是3,所以变量b的值也一定是3

但是如果是“线程1”、“线程3”、“线程2”这个顺序,那么最后“线程2”method2方法中的b值是3,还是4呢?其结果是可能是3,也可能是4。的确“线程3”在执行完method3方法后的确要unlock,然后“线程2”有个lock,但是这两个线程用的不是同一个锁,所以JMM这个两个操作之间不符合八大happen-before中的任何一条,所以JMM不能保证“线程3”对var变量的修改对“线程2”一定可见,虽然“线程3”先于“线程2”发生。

3. volatile变量规则:

对一个变量的写操作先行发生于后面对这个变量的读操作;

这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。

伪代码如下:

volatile int a;
a = 1;   //①
b = a'   //②

如果线程1 执行//①,“线程2”执行了//②,并且“线程1”执行后,“线程2”再执行,那么符合“volatile的happen-before原则”所以“线程2”中的a值一定是1

4. 传递规则:

如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

提现了happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C

5. 线程启动规则:

Thread对象的start()方法先行发生于此线程的每个一个动作;

6. 线程启动规则:

对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

7. 线程终结规则:

线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;

8.对象终结规则:

一个对象的初始化完成先行发生于他的finalize()方法的开始

总结

happen-before原则是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性。

下图是happens-before与JMM的关系图(摘自《Java并发编程的艺术》)

参考

Java内存模型之happens-before


# Java基础