double & operator[](int i)_深入解析单例模式之懒汉模式---Double-Check及volatile关键字

导读:在日常开发中对单例设计模式的应用十分常见,而看似简单小巧的设计模式其内部却蕴含着丰富的知识点。单例的创建方式有很多如懒汉模式和饿汉模式等、不同的语言又有不同的实现方式,但其本质的思想为:保证一个类仅有一个实例,并提供一个访问它的全局访问点。接下来,本文主要讨论在Java中单例模式之懒汉模式的实现及其double-check的思想、volatile关键字的作用。

单例模式--懒汉模式Demo

  • 将构造方法私有化,防止外界利用new创建此类的实例
  • 提供一个本类实例的唯一全局访问点
  • 不事先实例化对象,对象instance属性加上volatile关键字
  • 应用Double-check和同步锁保证了获取实例时线程安全和效率
/** * 懒汉模式 */public class SingletonTest {    private volatile static SingletonTest instance = null;    /**     * 将构造方法私有化,防止外部使用new创建实例     */    private SingletonTest() {    }    /**     * 提供一个获取实例的方法     */    public static SingletonTest getInstance() {               if (instance == null) {            synchronized (SingletonTest.class) {                if (instance == null) {                    instance = new SingletonTest();                }            }        }        return instance;    }}

单例模式--懒汉模式中的Double-Check的由来

懒汉模式中应用Double-Check,Double-Check的目的是保证获取实例时线程安全和效率。下面我们对SingletonTest类内部getInstance()方法进行分解,来演变Double-Check出现的过程。

359f40aef6233bfabf3806eb61a8043d.png

1、只有一个Check

修改getInstance()为只使用一个Check,通过该Check判断实例对象是否存在,如果不存在则进行实例化。但是在多线程的情况下,多个线程并发调用SingletonTest静态类执行getInstance()方法会导致出现重复实例化和覆盖问题。如:线程A先经过了Check但是还没来得及进行实例化,这时线程B也就顺利通过了Check,那么这就会导致两个线程都执行那段实例化的代码,重复实例化。

public static SingletonTest getInstance() {        if (instance == null) {                    instance = new SingletonTest();        }        return instance;    }

2、同步锁保证线程安全

造成上面这一问题根本原因在于:一段代码在多线程的情况下存在被多个线程同时执行的情况。那么解决这一问题的思路则是上同步锁,同步锁保证了同一时刻只能有一个线程执行同步代码块中的代码。

 public static SingletonTest getInstance() {            return instance;    }

3、第二个Check使性能提升

由于加上了同步锁,当有一个线程取得锁时其他线程就会处于阻塞的状态。虽然保证了线程安全,但是却对效率造成了影响。如:对象已经实例化了,其他线程想取得对象都要排队等待取得同步锁后才能取得对象。通过增加多一层Check,则可以优雅地解决此问题

getInstance

4、为什么要进行两次判断对象是否存在?

通过上面的过程我们得知Double-Check的演变:先进行一次Check--再加一层同步锁--再加一层Check,由内到外。那么有一个疑惑,最外层的Check是判断对象是否存在,里层的Check也是判断对象是否存在。从作用来看起来似乎冗余,那可否去掉里层的判断呢? 下面我们通过试验来得到该问题的答案。

4.1、把里层Check干掉

假设线程A和B并发调用getInstance(),线程A先通过了Check并取得锁但是还没来得及进行实例化。这时线程B也就可以顺利地通过Check并处于一个阻塞的状态。

3ea6475b88cc75cdfd6c9c01df70984f.png

加上同步锁虽然保证了线程安全,但是仍存在着重复实例化和覆盖问题。如:当线程A执行完毕后释放锁,线程B就会取得锁并执行同步块里的代码。这时如果没有加上第二次Check判断实例化对象是否存在则会导致线程B也进行实例化操作,结果导致发生重复实例化和覆盖。

c3016da23901965233c03126fbbb08e2.png

所以,Double-Check对于懒汉模式十分重要,既保证了线程安全也保证了执行效率。

eefc5e7e31c2a84e5d767d9b92bd21f3.png

为什么属性要用volatile关键字

单例模式另外一个关注点在于instance属性加上了volatile关键字,要理解为什么加上该关键字,首先需要理解下面这行简单代码内部究竟做了什么事情

instance = new SingletonTest();//在new一个对象过程按分为以下几个步骤
  1. 分配内存空间
  2. 初始化实例
  3. 设置instance指向刚分配的地址

而问题处在于第2和3步会出现重排序(重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段)既可能会出现重排序 从1-2-3 排序变为1-3-2,那么在多线程的情况下就会导致出现问题。

cd3c60b07e890d8af6901e89e1d73d24.png

下面用线程A和线程B举例

线程A执行到new SingletonTest(),开始初始化实例对象,由于存在指令重排序,这次new操作先执行了3把引用赋值了,还没有执行2初始化实例。这时时间片结束了,切换到线程B执行,线程B调用new SingletonTest()后发现引用不等于null,便直接返回引用地址了,这样会导致线程B访问到了一个还未初始化的对象。

解决方案:通过给instance增加volatile关键字之后,就保证new不会发生指令重排序,为被volatile关键字修饰的变量是被禁止重排序的。

最后

自此关于单例模式-懒汉模式的讨论结束。其中涉及到了Double-Check思想、同步锁解决线程安全问题、及使用volatile关键字解决重排序问题。单例模式看似简单,但其内部蕴含的知识点非常丰富,只有仔细和深入了解才能更好更安全地应用它。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值