DCL单例模式中,为什么要加volatile?

DCL:double check lock双重检查锁,如下面的代码

public class Singleton {
    private static volatile Singleton INSTANCE = null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(INSTANCE==null){//第一次检查
            synchronized (Singleton.class){
                if(INSTANCE==null){//第二次检查
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

为什么要加两次判空,第一次判空能不能不加?


效率问题:假设第一次判空不加,那么每次进入这个方法,INSTANCE不论是不是null,都会执行下面的synchronized代码块,多线程下会出现锁的竞争,而除了第一次初始化,后面的都不会为null,判空的效率比加锁高。

为什么要进行第二次判空?


防止多次初始化:多线程下,有可能会出现两个线程都经过了前面第一次检查,来到了下面的synchronized这里,如果不判空,就会出现一个线程new了一个Singleton出来,然后释放锁,第二个线程进来又会new一个Singleton出来。

volatile作用:1. 保持内存可见性,2.防止指令重排序
volatile这里的作用就是防止指令重排

INSTANCE = new Singleton();当使用new关键字创建一个对象时,JVM需要做哪些事情

1.为对象分配内存

     1.优先栈上分配

      2.栈中不能分配的,对象是否足够大?大的直接分配进老年代。

      3.TLAB中是否可以快分配?

      4.不能则在Eden区慢分配。

2.内存分配完毕,属性设置默认值(引用类型为null,基本类型为对应的默认值)。

3.执行构造函数,属性设置初始值。

4.建立连接,引用指向对象内存地址。

可以看到,new一个对象,虽然只有一行代码,实际上需要经过好几个过程,而且这些过程并非顺序执行。
有可能一个对象在未初始化时,就先建立连接了,一旦发生这种情况,使用DCL实现的单例模式,就会导致线程拿到的是一个未被初始化的对象。
想想看,未被初始化的对象,属性为引用类型则值全部为null,一旦对这些属性进行了操作,则会抛出空指针异常

volatile修饰的语句则禁止CPU这种乱序执行,保证指令执行的顺序性。

new 对象时的汇编指令

public class TestNewInstance {
    public static void main(String[] args) {
        Object o = new Object();
    }
}


将上面的代码编译后找到class文件所在目录,用javap -c TestNewInstance.class命令得到编译后的汇编指令。

       0: new           #2                  // class java/lang/Object  
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V  
       7: astore_1
       8: return


顺序的情况下,new指令申请了一块内存空间,invokespecial调用构造方法为对象进行初始化,astore_1将变量和新创建的对象关联起来。但是invokespecial和astore_1这两条指令没有关联性,所以astore_1有可能会跑到invokespecial前面执行。

如图,假设两个线程,线程1执行完astore_1时(此时instance已经指向一块内存地址,不为null,但是对象还未完成初始化),CPU切换到线程2执行if(instance==null),结果为false,于是返回了一个不完整的对象。使用volatile禁止指令重排就可以避免这种情况发生。

为什么volatile能禁止指令重排?

volatile的底层,与现在的CPU有关系,用cpu原语实现的,loadFence原语,storeFence原语

总结
在高并发的情况下,DLC双重检验不能保证由于指令重排导致的线程获取到未完全初始化对象的问题,需要利用volatile禁止重排的特性来保证单例的线程安全。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值