单例模式与volatile

单例模式为什么要加volatile

public class Singleton{

    private static Singleton singleton;

    

    private Singleton(){}

    

    public static  Singleton getInstance(){

        if (singleton ==null){   // w1阶段

            synchronize(Singleton.class){

                if (singleton == null){

                singleton  = new Singleton();// w2阶段

                }

            }

        }

        return singleton;

    }

}

以上代码看起来很完美吧,那我们来分析一下:

1.如果检查第一个singleton不为null,则不需要执行以下代码,提高了性能

2.如果第一个singleton为neull,即使有多个线程同一个时间判断,但是由于synchronize的存在,只会有一个线程能够创建对象

3.当第一个获取锁的线程创建完成后singleton对象后,其他的第二次判断singleton一定不为null,则直接返回已经创建好的singleton对象

是不是很完美的代码,逻辑上是完全没有问题的. 但是事实上并不是,上面的逻辑在jvm指令排序是存在问题的.jvm创建实例化对象的三个步骤,new 实例背后的指令这个被忽略的问题在于singleton = new Singleton();这行代码并不是一个原子指令。使用 javap -c指令,可以快速查看字节码。

从字节码可以看到创建一个对象实例,可以分为三步:

1.分配对象内存

2.调用构造器方法,执行初始化

3.将对象引用赋值给变量。

虚拟机实际运行时,以上指令可能发生重排序。以上代码 2,3 可能发生重排序,但是并不会重排序 1 的顺序。也就是说 1 这个指令都需要先执行,因为 2,3 指令需要依托 1 指令执行结果。

Java 语言规规定了线程执行程序时需要遵守 intra-thread semantics, 保证重排序不会改变单线程内的程序执行结果。这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。

虽然重排序并不影响单线程内的执行结果,但是在多线程的环境就带来一些问题。

上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例(w2阶段),这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入(w1阶段),由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。

由以上分析知道,由于重排序的原因,步骤2,3可能会发生重排序,其过程如下

memory = allocate();   //1: 分配内存空间instance = memory;     //3: 将内存空间的地址赋值给对象的引用

ctorInstance(memory);  //2: 初始化对象

以上的排序就会导致第二个判断会出错 singleton !=null 但是这时该对象只是个内存地址,并没有初始化实例,所以return的是singleton的内存地址,导致第二个线程获取到的是singleton的内存地址,引用起来就会抛错。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值