随着对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并发编程的艺术》)