引用
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() 这句话可以分为三步:
- 为 singleton 分配内存空间;
- 初始化 singleton;
- 将 singleton 指向分配的内存空间。
但是由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。 指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得一个未初始化的实例。例如:线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。
使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行。