happens-before的定义
先行发生是Java内存模型中定义的两项操作数之间饿的偏序关旭,如果操作A先行发生于操作B,其实就是在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。 先行发生是判断是否存在竞争、线程是否安全的主要依据,依据这个原则,我们可以通过几条规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题。
特性
程序顺序性规则
一个线程中的每个操作,happens-before于该线程中的任意后续操作。简单来说就是,(多线程环境下)只要执行结果不被变更,无论怎么“排序”都是对的。
volatile变量规则
对一个volatile域的写,happens-before (先行发生)于任意后续对这个volatile域的读
为了解释上面两个规则,用下面程序进行说明
public class ReorderExample {
private int x = 0;
private int y = 1;
private volatile boolean flag = false;
public void writer(){
x = 42; //1
y = 50; //2
flag = true; //3
}
public void reader(){
if (flag){ //4
System.out.println("x:" + x); //5
System.out.println("y:" + y); //6
}
}
}
这里涉及到了volatile的内存增强语意,先来看表格
能否重排序 |
第二个操作 |
第二个操作 |
第二个操作 |
---|---|---|---|
第一个操作 |
普通读/写 |
volatile 读 |
volatile 写 |
普通读/写 |
- |
- |
NO |
volatile 读 |
NO |
NO |
NO |
volatile 写 |
- |
NO |
NO |
从这个表格最后一行,可以看出:
如果第二个操作为volatile写,不管第一个操作是什么,都不能重排序,这就确保了“volatile写之前的操作不会被重排序到volatile写之后” ,拿上面的代码来说,代码1和代码2不会被重排序到代码3的后面,但是代码1和2可能被重排序(没有依赖也不会影响到执行结果),说到这里和“程序顺序性规则”是不是已经关联起来了呢?
从表格的倒数第二行可以看出 :
如果一个操作为volatile读,不管第二个操作是什么,都不能被重排序,这确保了volatile读之后的操作都不会被重排序到volatile读之前。拿上面的代码来说,代码4是读取volatile变量,代码5、6不会被重排序到代码4之前。
传递性规则
如果A happens-before B,且B happens-before C ,那么A happens-before C
从上面图可以看出
-
x=42和y=50 happens-before flag=true ,这就是规则1
-
写变量(代码3) flag=true happens-before读变量(代码4) if(flag) ,这是规则2
根据规则3传递性规则,x=42 happens-before 读变量if(flag)
谜案要揭晓了:
如果线程B读到了flag是true,那么x=43 和y=50对线程B就一定是可见了
通常而言,上面三个规则是一种联合约束。
监视器锁规则
对一个锁的解锁 happens-before于随后对这个锁的枷锁
这个规则我觉得大家应该非常熟悉来,就是解释synchronized关键字
public class SynchronizedExample {
private int x = 0;
public void synBlock(){
// 1.加锁
synchronized (SynchronizedExample.class){
x = 1; // 对x赋值
}
// 3.解锁
}
// 1.加锁
public synchronized void synMethod(){
x = 2; // 对x赋值
}
// 3. 解锁
}
先获取锁的线程,对x进行赋值之后再释放锁,另外一个线程再获取锁,一定能看到对x的赋值的改动,就是这么简单。可以用下面命令查看上面程序,看同步块和同步方法转换成还变指令有何不同!
javap -c -v SynchronizedExample
start规则
如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作 happens-before于线程B中的任何操作,也就是说:主线程A启动来线程B后,子线程B能看到主线程再启动子线程B前的操作。
public class StartExample {
private int x = 0;
private int y = 1;
private boolean flag = false;
public static void main(String[] args) throws InterruptedException {
StartExample startExample = new StartExample();
Thread thread1 = new Thread(startExample::writer, "线程1");
startExample.x = 10;
startExample.y = 20;
startExample.flag = true;
thread1.start();
System.out.println("主线程结束");
}
public void writer(){
System.out.println("x:" + x );
System.out.println("y:" + y );
System.out.println("flag:" + flag );
}
}
运行结果:
主线程结束
x:10
y:20
flag:true
Process finished with exit code 0
线程1看到来主线程调用thread1.start()之前的所有赋值结果,这里没有打印【主线程结束】,这个和守护线程知识有关。
join()规则
如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回,和start规则刚好相反,主线程A等待子线程B完成,当子线程B完成后,主线程能够看到子线程B的赋值操作。
public class JoinExample {
private int x = 0;
private int y = 1;
private boolean flag = false;
public static void main(String[] args) throws InterruptedException {
JoinExample joinExample = new JoinExample();
Thread thread1 = new Thread(joinExample::writer, "线程1");
thread1.start();
thread1.join();
System.out.println("x:" + joinExample.x );
System.out.println("y:" + joinExample.y );
System.out.println("flag:" + joinExample.flag );
System.out.println("主线程结束");
}
public void writer(){
this.x = 100;
this.y = 200;
this.flag = true;
}
}
运行结果:
x:100
y:200
flag:true
主线程结束
Process finished with exit code 0
「主线程结束」这几个字打印出来喽,依旧和线程何时退出有关系
总结
-
Happens-before 重点是解决前一个操作结果对后一个操作可见,相信到这里,你已经对Happens-before规则有所了解,这些规则解决来多线程编程的可见性于有序性问题,但还没有完全解决原子性问题(除synchronized)
-
start 和join规则也是解决主线程与子线程通信的方式之一
-
从内存语意角度来说,volatile的【写-读】与锁的【释放-获取】有相同效果;volatile写和锁的释放有相同的内存语义;volatile读与锁的获取有相同内存语义。volatile解决的是可见性问题,synchronized 解决的是原子性问题,两则不应该混为一谈。