单例模式

详解双重判空懒汉式单例

Posted by Static on June 19, 2020

1. What?

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象

单例模式又分 懒汉式饿汉式:

懒汉式: 在类加载时不会初始化,而在需要使用时进行实例化。

优点是初始化时减少内存空间,在并发下可能存在的问题,代码中会详细介绍并发时如何避免

饿汉式: 在类加载时实例化。

不会存在并发问题,不过初始化时占用了内存(这点内存可以忽略不计,推荐饿汉式单例)

2. Why?

  1. 单例类只能有一个实例。

  2. 单例类必须自己创建自己的唯一实例。

  3. 单例类必须给所有其他对象提供这一实例。

在不需要创建新对象时,避免创建不必要的内存空间,可以使用单例模式实例化对象

3. How?

1. 饿汉式

/**
 * 饿汉式
 */
public class HungrySingle {

    private final static HungrySingle INSTANCE=new HungrySingle();

    private HungrySingle(){}

    public static HungrySingle getInstance(){
        return INSTANCE;
    }

}

枚举,枚举默认是单例的,原因: 传送门

/**
 * 饿汉式(枚举)
 */
public enum Single {
    INSTANCE
}

2. 懒汉式

a. 为什么用synchronized?b. 为什么双重判空? c. 为什么要用volatile修饰?

/**
 * 懒汉式
 */
public class LazySingle {

    // volatile作用:
    //      a. 避免指令重排序;
    //      b. 保证变量可见性,不能保证原子性
    //      c. 内存屏蔽
    private static volatile LazySingle instance;

    private LazySingle() {}

    // 前提:多线程情况下
    // 为了保证线程安全,使用同步锁机制和双重判空逻辑
    public static LazySingle getInstance() {
        // 第一层判空逻辑,拦截已经初始化的调用
        if (instance == null) {
            // 添加同步锁机制,即多线程进入代码,排队等待执行
            synchronized (LazySingle.class) {
                // 第二层判断的原因:多线程进入,可能存在两个线程同时执行第一层判空都进入代码块,则需要再次判断是否为空,若没有判空逻辑,则会出现创建多个实例的问题
                if (instance == null) {
                    // jvm层面创建对象过程:1. 开辟堆上内存空间,-> 2. 调用构造方法,-> 3. 分配指针指向实例对象的内存地址,即非原子操作
                    // 由于编译优化,2、3操作没有依赖关系,可能执行顺序1->3->2,也可能是1->2->3,不过volatile可避免指令重排序,只允许顺序执行:1->2->3
                    // 不添加volatile在单线程情况下 1->3->2 执行没问题,不过在多线程下可能会有问题
                    // 比如线程A执行1操作时,时间片用完,这时线程B进入,执行1->3,时间片用完,线程A继续执行,发现不为空,直接返回,其实还没有执行构造方法,对象还是没有实例化
                    instance = new LazySingle();
                }
            }
        }
        return instance;
    }
}