happen-before原则

1,124 阅读5分钟

引用

www.jianshu.com/p/b9186dbeb…

1.为什么要有一个happens-before的原则

1.1 为什么要有一个happens-before的原则?

结论:happens-before觉得着什么时候变量操作对你可见。

我们知道cpu的运行极快,而读取主存对于cpu而言有点慢了,在读取主存的过程中cpu一直闲着(也没数据可以运行),这对资源来说造成极大的浪费。所以慢慢的cpu演变成了多级cache结构,cpu在读cache的速度比读内存快了n倍。

当线程在执行时,会保存临界资源的副本到私有work memory中,这个memory在cache中,修改这个临界资源会更新work memory但并不一定立刻刷到主存中,那么什么时候应该刷到主存中呢?什么时候和其他副本同步? 而且编译器为了提高指令执行效率,是可以对指令重排序的,重排序后指令的执行顺序不一样,有可能线程2读取某个变量时,线程1还未进行写入操作。这就是线程可见性的来源。

1.2 happens-before原则有啥好处?

j 是否等于1呢?假定线程A的操作(i = 1)happens-before线程B的操作(j = i)。 那么可以确定线程B执行后j = 1 一定成立。 如果他们不存在happens-before原则,那么j = 1 不一定成立。 (即使代码是先执行j=1,然后执行j=i,也不一定j=1,主要看是否符合happens-before)

1.3如何判断是否为 happens-before?

  • 程序次序规则: 在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作 (同一个线程中前面的所有写操作对后面的操作可见)
int a = 1;
int b= 2;
hb(a,b)
  • 管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序)对同一个锁的lock操作。 (如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程))
线程T1:
a = 1;
lock.unlock();
b = ture;

线程T2:
if (b) {
    lock.lock();
    int c = a;
    System.out.println(c);
    lock.unlock();
}

b赋值之前加了解锁操作,线程T2在读取到b值变更后,做了加锁操作。这时候就是第二条原则生效的时候,它告诉我们,假如在时间上T1的lock.unlock()先执行了,T2 的lock.lock()后执行,那么T1 unlock之前的所有变更,a=1这个变更,T2是一定可见的,即T2 在 lock后,c拿到的值一定是 a 被赋值1的值。因为 a = 1 和  lock.unlock() 有  hb 关系  hb(a=1 ,   lock.unlock() )第二条原则  hb(unlock, lock),  而  hb(lock , c = a ),因此c在被赋值a时,a=1一定会先行发生。

  • volatile变量规则:对一个volatile变量的写操作happen—before后面(时间上)对该变量的读操作。 (如果线程T1写入了volatile变量v(vv=222),接着线程T2读取了vv,那么,线程1写入vv及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)).
//共享变量a, b, c
//线程T1: 
a = 111;
vv = 222;//volatile value

//线程T2:
int ff = vv;// ff 是222
int c = a;  //c就是111 
  • 线程启动规则:Thread.start()方法happen—before调用用start的线程前的每一个操作。 (假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行前对线程B可见。注意:线程B启动之后,线程A在对变量修改线程B未必可见。)
//线程A
a = 1;
//线程B启动
B.start();  //如果线程B里面使用共享变量a,将能够读取a=1,之前A线程的写入操作全部可见
  • 线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。 (线程t1写入的所有变量,在任意其它线程t2调用t1.join(),或者t1.isAlive() 成功返回后,都对t2可见。)
//t1线程
a = 1;

t2线程
t1.join();
int c = a;   //t1写入的都对t2可见,能够读取到a的值为1;
  • 线程中断规则:对线程interrupt()的调用 happen—before 发生于被中断线程的代码检测到中断时事件的发生。 (线程t1写入的所有变量,调用Thread.interrupt(),被打断的线程t2,可以看到t1的全部操作)
线程1:
a = 1;
Thread.interrupt();

线程2:
t1.isInterrupt()
c = a;  //可以读取到a=1;
 
  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。 (对象调用finalize()方法时,对象初始化完成的任意操作,都可见)
public class foo {
    private int a = 1;
    public foo () {
        this.a = 2;
    }
    
    public finalize () {
        c = a;   //a = 2,构造器中的写入, finalize能够读取
    }
}
  • 传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

2. 应用

2.1 DCL

public class Singleton(){  
    private volatile static Singleton singleton;  //为什么使用volatile 
    private Sington(){};  
    public static Singleton getInstance(){  
        if(singleton == null){  
            synchronized (Singleton.class){  
                if(singleton == null){  
                     singleton = new Singleton();    
                }  
            }
        }           
        return singleton;  
    }  
}  

singleton = new Singleton() 这句话可以分为三步:

  1. 为 singleton 分配内存空间;
  2. 初始化 singleton;
  3. 将 singleton 指向分配的内存空间。

但是由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。 指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得一个未初始化的实例。例如:线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。

使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行。