欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

并发编程必知必会——Happens-before

时间:2023-06-18
一段代码告诉你什么是并发编程的bug

public class ReorderExample { private int x = 0; private int y = 1; private boolean flag = false; public void writter() { x = 42; y = 50; flag = true; } public void reader() { if (flag) { System.out.println("x=" + x + " y=" + y); } }}

可以看到上面这段代码,writter负责写,reader负责读,单线程情况下顺序执行这两个方法没有问题。多线程情况下,线程1执行写,线程2执行读会发生什么呢?请看下图:


为了保证程序运行的效率,编译器常常会擅自优化语句,线程1执行的writter很可能会优化成上图左边所示代码,在多线程情况下,假如执行到flag=true,直接切换到线程2,这就会导致预期输出的x,y应该是42,50,结果却是42,1。

解决方案:将flag声明语句中加一个volatile

public class ReorderExample { private int x = 0; private int y = 1; private volatile boolean flag = false; public void writter() { x = 42; //代码1 y = 50; //代码2 flag = true;//代码3 } public void reader() { if (flag) {//代码4 System.out.println("x=" + x + " y=" + y);//代码5 } }}

你一定被我的操作搞得一脸懵逼,为什么这样一个关键字就能避免指令重排呢?这时候我就可以给你好好掰扯掰扯大名顶顶的happens-before原则了。


这样一个表格是不是直接给你干懵了?其实就可以翻译成一句人话:只要第一个操作是volatile读,或第二个操作volatile写就不会发生指令重排。

以上面的代码为例,代码1和代码2只是普通写(可以把这两个操作当作普通读/写,他们随意指令重排对结果没有任何影响),而代码3为volatile写(第二个操作),根据上面那句人话:“第二个操作volatile写就不会发生指令重排”。
代码4是volatile读,代码5是普通读写,根据上面的人话:“只要第一个操作是volatile读,就不会发生指令重排”

这样就保证了代码的有序性。那么可见性呢?请看笔者画的又一张图。


可以看到加了volatile关键字的线程1保证了在他前面的操作永远不会到他后面,如果我们把代码1、代码2看作一个代号为A的操作,再把flag=true看作代号为B的操作,线程2的if(flag)看作代号C的操作的话。再结合表格我们可以看出:

A是普通写,B是volatile写,那么A就happens-before于B。

而C是volatile读,根据表格可以看出:

B是volatile写,C是volatile读,他们也不会发生指令重排,那么B就happens-before于C。

经过这样一段推倒,我们就可以得到Happends-before的有一个废话原则,那就是传递性规则:

A happens-before B, 且 B happens-before C, 那么 A happens-before C

这样就保证了可见性,至此打完收工

详述happends-before其他守则 监视器锁规则

对⼀个锁的解锁 happens-before 于随后对这个锁的加锁

这也是一句废话,后来人上锁必须等前人解锁才行,所以后来人加锁时的对你的操作永远是可见的。就以下面代码为例,你每次上锁、解锁后x变为多少你永远都知道的。

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、解锁}

start()规则

package com.example.volatileOpr;public class StartExample { private int x = 0; private int y = 1; private boolean flag = false; public void reader() { if (flag) {//代码4 System.out.println("x=" + x + " y=" + y);//代码5 } } public static void main(String[] args) { StartExample startExample = new StartExample(); Thread thread = new Thread(startExample::reader); startExample.x = 0; startExample.y = 0; startExample.flag=true; //子线程start在主线程一顿操作后start,那么他就能看到start的操作就对他可见 thread.start();//x=0 y=0 System.out.println("主线程结束"); }}

说白了,就是主线程在子线程start前的任何操作,子线程都能看到,翻译成鬼话就是:

如果线程 A 执⾏操作 ThreadB.start() (启动线程B), 那么 A 线程的

ThreadB.start() 操作 happens-before 于线程 B 中的任意操作join()规则

package com.example.volatileOpr;public class JoinExample { private int x = 0; private int y = 1; private boolean flag = false; public void writter() { x = 42; //代码1 y = 50; //代码2 flag = true;//代码3 } public static void main(String[] args) throws InterruptedException { JoinExample joinExample = new JoinExample(); Thread thread = new Thread(joinExample::writter); thread.start(); //加了join后主线程永远需要等到子线程结束后才能结束,这意味着子线程所有的操作对主线程都是可见的 thread.join(); System.out.println("主线程结束 x=" + joinExample.x + " y=" + joinExample.y);//x=42 y=50 }}

join规则和start规则差不多,说白了就是子线程用了join后所有的操作都对主线程可见,翻译成鬼话就是:
如果线程 A 执⾏操作 ThreadB.join() 并成功返回, 那么线程 B 中的任意操作happens-before 于线程 A 从 ThreadB.join() 操作成功返回
打完收工,欢迎来笔者的源码地址

https://gitee.com/fugongliudehua/concurrency

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。