Happen-before原则的浅谈

一、Happens-Before模型

除了我们常见的显示引用volatile关键字能够保证可见性以外,在Java中,还有很多的可见性保障的规则。
从JDK1.5开始,引入了一个happens-before的概念来阐述多个线程操作共享变量的可见性问题。所以我们可以认为在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在happens-before关系。这两个操作可以是同一个线程,也可以是不同的线程。

程序顺序规则(as-if-serial语义)

a.不能改变程序的执行结果(在单线程环境下,执行的结果不变)
b.依赖问题, 如果两个指令存在依赖关系,是不允许重排序

对于规则a来说是必须要保障的,那么b规则的理解参考下面的代码理解

int a=0;
int b=0;
void test(){
  int a=1;      //(1)
  int b=1;      //(2)
  int c=a*b;    //(3)
}

因为c依赖a和b变量的值,所以一定a happens -before c ; b happens before c

传递性规则

假设a happens-before b ,b happens- before c, 那么一定存在a happens-before c

volatile变量规则

volatile 修饰的变量的写操作,一定happens-before后续对于volatile变量的读操作.
内存屏障机制来防止指令重排,前面我们在volatile原理实现分析过,不再多说

监视器锁规则

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

int x = 10;
synchronized (this) { // 此处自动加锁
	// x 是共享变量, 初始值 =10
	if (this.x < 12) {
	this.x = 12;
	}
} // 此处自动解锁

假设 x 的初始值是 10,线程 A 执行完代码块后 x 的值会变成 12(执行完自动释放锁),线程 B 进入代码块时,能够看到线程 A 对 x 的写操作,也就是线程 B 能够看到 x==12。
所以针对前面volatile关键字可见性的实例,其实可以不用关键字也可以实现,如下

public class VolatileDemo {
    // 添加volatile关键字,解决可见性问题
    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            int i = 0;
            while (flag){
                i++;
                // 通过锁的释放来保证可见性
                synchronized (VolatileDemo.class){
                }
            }
        });
        thread.start();;
        Thread.sleep(1000);
        flag = false;
    }
}

start规则

public class StartDemo{
  int x=0;
  Thread t1=new Thread(()->{
    //读取x的值 一定是20
    if(x==20){
    ...
   }
 });
  x=20;
  t1.start();
}

Join规则

public class Test{
  int x=0;
  Thread t1=new Thread(()->{
    x=200;
 });
  t1.start();
  t1.join(); //保证结果的可见性。
  //在此处读取到的x的值一定是200.
}

二、DCL半对象问题

由Happen-before模型引出的一个非常经典的问题,就是DCL半对象问题。

什么是DCL半对象问题?

我们过一个例子说明,我们学设计模式的时候学过恶汉模式,代码如下

public class LazySingleton {
    private int random;
    
    private static LazySingleton instance;
    
    private LazySingleton() {
        this.random = new Random().nextInt(100);             // (1)
    }
    public static LazySingleton getInstance() {
        if (instance == null) {                              // (2)
            synchronized(LazySingleton.class) {              // (3)
                if (instance == null) {                      // (4)
                    instance = new LazySingleton();          // (5)
                }
            }
        }
        return instance;                                     // (6)
    }

    public int getRandom() {
        return this.random;                                  // (7)
    }
}

假设线程Ⅰ先调用getInstance(),由于此时还没有任何线程创建LazySingleton实例,它会创建一个实例instance并返回。这时线程Ⅱ再调用getInstance(),当它运行到(2)处,由于这时读instance没有同步,它有可能读到instance或者null。
先考虑它读到instance的情形:
线程Ⅱ观察到了线程Ⅰ对instance的写入,这时线程Ⅱ就会执行语句(6)直接返回这个instance的值
然后对这个instance调用getRandom()方法,该方法也是在没有任何同步情况被调用,因此整个线程Ⅱ的操作都是在没有同步的情况下调用 ,这时我们无法得到线程Ⅰ的操作和线程Ⅱ的操作之间的任何有效的happen-before关系
这说明线程Ⅰ的语句(1)和线程Ⅱ的语句(7)之间并不存在happen-before关系,这就意味着线程Ⅱ在执行语句(7)完全有可能观测不到线程Ⅰ在语句(1)处对random写入的值,这就是DCL的问题
再考虑它读到null的情形:
线程Ⅱ没有观察到了线程Ⅰ对instance的写入,那么它会执行(3)(6),加同步锁,此时肯定会有线程Ⅱ(3)happen-before 线程Ⅰ的(1),那么就会有 线程Ⅱ(6)happen-before 线程Ⅰ的(1),然后线程Ⅱ再调用(7)肯定没问题

那怎么解决这个问题呢?

private static volatile LazySingleton instance; // 添加volatile关键字

本文是综合自己的认识和参考各类资料(书本及网上资料)编写,若有侵权请联系作者,所有内容仅代表个人认知观点,如有错误,欢迎校正; 邮箱:[email protected] 博客地址:https://blog.csdn.net/qq_35576976/

你可能感兴趣的:(多线程)