美文网首页
Happens-Before规则与DCL失效原因分析

Happens-Before规则与DCL失效原因分析

作者: LeonardoEzio | 来源:发表于2019-03-17 11:36 被阅读0次

    先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值/发送了消息/调用了方法等。

    Happens-Before规则(先行发生原则)

    1. 程序次序规则(Program Order Rule):

    在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。如下 value = 4 语句 happens-before flag = true 语句;

    public class HappensBefore {
    
        static int value = 0;
        static volatile boolean flag = false;
    
        public  void write(){
            value = 4;
            flag = true;
        }
    
        public  void read(){
            if(flag){
                System.out.println(value);
            }
        }
    }
    
    2 .管程锁定规则(Monitor Lock Rule):

    管程—一种通用的同步语句,在Java中主要指的就是Synchronized。而管程锁定规则指的就是对一个锁的unlock happens-before 后续对这个锁的lock操作。如下,若线程A、B同时访问read方法,当线程A执行完之后,线程B能够获取到线程A对变量的操作。

    public class HappensBefore {
    
        int value = 10;
    
        public void read(){
            synchronized (this){ //此处加锁
                if(value < 100){
                    value ++;
                }
            }//此处自动解锁
        }
    }
    
    3.volatile变量规则(Volatile Variable Rule):

    对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”是指时间上的先后顺序。

    4.传递性(Transitivity):

    如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。

    第3点以及第4点都采用第一段代码做示例,首先从第一段代码我们可以知道value = 4 happens-before flag = true;然后可以得知对volatile 型变量flag 的写操作 happens-before 对其的读操作。因此根据传递性规则可以得知 value = 4 happens-before flag = ture的读操作。因此当线程B读到了flag=true时,那么线程A对value所做的更改对B线程也是可见的。这得益于jdk1.5之后对volatile关键字的语义增强(可见性,与禁止指令重排序)。

    5.线程启动规则(Thread Start Rule):

    Thread对象的start()方法先行发生于此线程的每一个动作。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程B前的操作。换句话说就是,如果线程A调用线程B的Start()方法(在线程A中启动线程B),那么该start()操作happens-before于线程B中的任意操作。

    public class HappensBefore{
    
        static int value = 10;
    
        public static void main(String[] args) {
    
            Thread newThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    //线程启动前,所有对共享变量的修改,此处都可见,因此这里的value=66
                    System.out.println(value);
                }
            });
            value = 66;
            newThread.start();
        }
    }
    
    6. 线程 join() 规则:

    指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线B的Join()方法实现),当子线程B完成后(主线程A中Join方法返回),主线程能够看到子线程的操作。也就是如果在线程A中,调用线程B的Join()并成功返回,那么线程B中的任意操作Happens-Before于该Join操作的返回。如下:

    public class HappensBefore{
    
        static int value = 10;
    
        public static void main(String[] args) {
    
            Thread newThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    //此处对共享变量进行修改
                    value = 666;
                }
            });
    
            newThread.start();
            try {
                newThread.join();
                System.out.println(value);//子线程对共享变量的修改在子线程调用Join之后皆可见,此处value=666
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    DCL(双锁检查机制)实现单例模式失效分析

    先看一段双锁检查实现单例模式的代码:

    //Double-check-lock 单例模式失效原因详解
    public class Singleton {
        
        static Singleton instance;// 使用volatile可以禁止指令的重排序
        
        static Singleton getInstance(){
            if(instance == null){
                synchronized (Singleton.class){
                    if (instance == null){
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    假设此时同时有线程A、以及线程B同时调用getInstance()方法来获取Singleton的实例。这段代码看起来不存在什么问题,但是并不完美,缺陷就在进行 instance = new Singleton()操作的时候。一般情况下我们所理解的new操作会进行如下的操作:

    1. 分配一块内存M
    2. 在内存M上初始化Singleton对象
    3. 将M的地址值赋给instance

    然后经过编译器优化(指令重排序)的操作如下:

    1. 分配一块内存M
    2. 将M的地址值赋给instance
    3. 在内存M上初始化Singleton对象
    具体示意图如下: DCL.png

    假设当线程A调用getInstance()方法时 instance == null 此时就会执行new Singleton()操作,此时的指令是已经经过编译器优化的了,假设当执行到
    Instance = M 即将M的地址值赋给 instance的时候,线程B获得了执行权,当线程B再去调用getInstance()方法时instance != null 此时线程B就有可能会获得未经过初始化的Singleton对象,从而导致NullPointException异常的发生。关于这种情况的解决办法,可以使用Volatile关键字来进行规避,采用Volatile关键字之后,计算机对Instance对象进行操作时,会加入内存屏障从而禁止编译器对相应的指令进行重排序。

    相关文章

      网友评论

          本文标题:Happens-Before规则与DCL失效原因分析

          本文链接:https://www.haomeiwen.com/subject/pnpymqtx.html