单例模式问题详解

本文中主要阐述一下懒汉模式实现中的各种问题

一、单例模式有两种实现方式:

  1. 饿汉模式
  2. 懒汉模式(延迟加载)

二、实现单例模式中比较重要的三个点是:

  1. 构造方法私有
  2. 定义静态成员变量
  3. 对外提供静态方法

三、第一个单例:

下面的单例中,构造方法私有,静态成员在变量在静态方法中提供出去了,并且在方法中加上了synchronized关键字,保证了线程安全

package com.scott;

/**
 * 单例模式
 */
public class Singleton {

    /** 使用 volatile 保证实例变量的原子性操作 */
    private volatile static Singleton instance;
    /** 私有化构造方法 */
    private Singleton() { }

    /**
     * 获取实例
     * @return 实例
     */
    public static synchronized Singleton getSingletonInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

上面这个无疑是一个单例,并且是线程安全的。方法上面为什么加了synchroized

如果不加synchroized锁会发生什么情况呢?下面来分析一下:

  1. 假如存在线程A线程B同时调用了getSingletonInstance方法
  2. 线程A执行了 if(instance == null) 后,结果为true 但是CPU时间片用完了,线程A将CPU执行权交出去了
  3. 此时线程B获取到了CPU的执行权,线程B执行。线程B判断 if(instance == null) 也为 true因为线程A还没有创建Singletong对象线程B继续执行完整个方法,所以线程B创建了一个Singleton对象。
  4. 此时线程A获取到了CPU的执行权,继续执行instance = new Singleton(),此时线程A也创建了一个Singleton对象

但是上面这个单例存在一个问题synchroized太重了,性能相对来说不够高。所以下面使用双重检查锁来实现单例模式

四、第二个单例(第一版双重检查锁)

/**
 * 单例模式
 *
 * 使用 synchronized 保证在多线程下依旧是同一个实例。
 * 双重检查加锁,减少加锁的性能消耗
 */
public class Singleton {

    /** 静态私有变量 */
    private static Singleton instance;
    /** 私有化构造方法 */
    private Singleton() { }

    /**
     * 获取实例
     * @return 实例
     */
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

如上述代码中所示,这里面存在两个判空。通过多次判空来保证对象是否存在。为什么需要两个判空操作:

  1. 线程A和线程B同时来调用getInstance()方法
  2. 线程A获取到了锁,线程B还没获取到锁,但是线程B已经执行了第一个为空判断,线程B此时在等待锁
  3. 线程A执行完整个方法,获取到了Singleton对象,并且释放了锁
  4. 线程B获取到了锁,此时如果不再进行一次为空判断,则线程B会重复的创建一个Singleton对象。

五、volatile对象保证可见性和原子性

如上面第四点中的代码所示,虽然保证了单例,但是代码还是存在问题的。可能会出现获取到空对象的情况。

假如存在三条线程同时访问getInstance()方法:

  1. 线程A和线程B都已经执行到sychronized所在的行,此时一定是其中一条线程获取到锁对象,假设此时线程A获取到了锁
  2. 线程A获取到了锁,线程A继续执行了instance == null行,在执行instance == new Singleton()行时,发生了指令重排,
    1.  这里说明一下 new Singleton()做了什么事:
      1. 申请了内存空间
      2. 完成属性的初始化
      3. 将内存地址交给instance变量保存
  3. 线程A先将内存地址交给instance变量保存了,但是还没有完成属性的初始化。
  4. 此时在线程A完成new Singleton()的属性初始化之前线程A的CPU时间片用完了,
  5. 此时三个线程的状态分别是:线程C进来了,线程B在获取锁的地方被阻塞,线程A没有时间片
  6. 线程C执行第一个为空判断,发现对象已经存在了,直接返回使用了

为了防止这种情况下的异常,所以静态成员变量使用volatile修改,volatile可以保证变量的可见性和原子性操作

可见性使用MESI协议(内存一致性协议)保证了变量在多个CPU高速缓存区中的可见性

volatile保证了不会轻易的出现指令重排序。new Singleton() 最终执行时是通过多个字节码指令执行的

所以在上述第四点的代码中静态变量还需要使用volatile修饰。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值