java基础总结(二十七)-- 单例模式的创建方式之一双检索,有什么缺陷吗?

来自:https://blog.csdn.net/a_842297171/article/details/79316591

 

这几天看并发编程的书,发现原先写的单例模式有点问题,当时认为双重检查是安全的,现在有新的了解。下面是双重检查写法:

public static LasyModeSingletonVersion4 getSole() {
        //只有sole为空时才构造,否则直接返回
        if(null == sole) {
            //在构造的临界区上加锁,而不是整个方法加锁
            synchronized(LasyModeSingletonVersion4.class) {
                //获得锁之后继续再一次判断,这样就安全了
                if(null == sole) {
                    sole = new LasyModeSingletonVersion4();
                }

            }
        }
        return sole; 
    }

•乍看之下,发现挺完美:一个线程获得同步锁之后在还没构造完成对象之前,其他线程永远只能阻塞在构造语句之外,且还有一层双重保险:再加一次校验。 
•直到看到指令重排序,上面的程序我都认为是很安全的。指令重排序是JVM对语句执行的优化,只要语句间没有依赖,那JVM就有权对语句进行优化。比如:

int a = 1;//①
int b = 1;//②
int c = a + b;//③

很简单的a+b,我们写代码在脑中构想或者进行断点调试都是这样一个语句一个语句执行的,但是jvm在执行的时候却不一定按这个顺序。在这里要说下as-is-serial这个东西,as-if-serial是指在执行结果不会改变的情况下,JVM为了提高程序的执行效率会对指令进行重排序。就像上面的程序,为了保证程序的正确性,③是一定要在①和②之后执行的,但是①和②则就没有依赖了,①和②谁先执行都不影响结果,所以JVM就可能会对它们进行重排序。as-if-serial保证了单线程下程序执行结果的正确性。 
•再看看双重检查的代码,synchronized块里面的代码我们可以看成是单线程程序,因为同一时刻只有一个线程在执行,里面的语句很简单,就是判断是否为空,如果为空则构建对象,现在需要思考的是构建对象是否可能会被JVM重排序,很遗憾,sole = new LasyModeSingletonVersion4();这条语句并没有原子性,就像i++;一样,它是被分为好几步完成的:
 

①分配空间给对象
②在空间内创建对象
③将对象赋值给引用sole

•上面的语句中,②是依赖于①的,所以②在①之后执行,但是③和②不存在依赖性,也就是执行顺序可能是:①->③->②,如果是单线程的程序(真的只有一个线程可以访问到它们),那么如果后续程序使用到了sole,JVM会保证你使用sole的时候是初始化完成的,但是现在在synchronized块之外有其它线程“虎视眈眈”,获取到锁的线程如果按照①->③->②的顺序执行,那在执行③的时候会store-write,即将值写回主内存,则其它线程会读到最新的sole值,而现在这个sole指向的是一个不完全的对象,即不安全对象,也不可用,使用这个对象是有危险的,此时构造对象的线程还没有释放锁,其它线程进行第一次检查的时候,null == sole的结果是false,会返回这个对象,造成程序的异常。 
•一说到指令重排序,我们很容易想到volatile关键字,volatile禁止指令重排序,所以如果sole被volatile修饰的话,可以保证这个初始化的有序性。

•但是今天想介绍的是一个更好的单例模式,它是“天生”线程安全的:
 

class BetterSingleton{
    static{
        sole = new BetterSingleton();
    }

    public static BetterSingleton sole;
    private BetterSingleton() {
        System.out.println(new Random().nextInt(100000));
    }
    public static BetterSingleton getSole() {
        return sole;
    }
}

•看上去很简单,将构建对象放在了static块中,这里就要说到< clinit >方法,它会在JVM初始化类时调用,包括静态变量初始化语句和静态块的执行,而虚拟机会保证类只保证初始化一次,所以类的初始化是单线程执行的,所以将单例实例化放在static块是天生线程安全的。多线程环境下调用getSole()方法时,第一个调用的线程会引起类的初始化。所以这个单例是目前比较好的,当然还有枚举的单例。
--------------------- 
作者:幺零小柒 
来源:CSDN 
原文:https://blog.csdn.net/a_842297171/article/details/79316591 
版权声明:本文为博主原创文章,转载请附上博文链接!

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值