Bootstrap

【设计模式】单例模式

单例模式

1. 概念

单例模式是设计模式的一种,要求在一个进程中,被指定的类只能创建唯一一个实例

举个栗子🌰

有时候我们需要为一个类创建一个实例来管理我们的数据,如果这个时候这个实例管理的数据非常多时,比如10GB,一个实例就已经很占我们的资源空间了,太多的话机器可能就顶不住了,这个时候就需要我们约定好一个创建规则,如这个类只能被实例化一次,若该类被多次实例化,则视为异常情况,这就是单例模式!

实现单例模式的方式有很多种,这里介绍两种最基础的实现方式:饿汉模式懒汉模式

2. 饿汉模式

饿汉模式的“饿”字,其实想表达的是创建实例的时间非常早,非常迫切在类加载阶段就完成了对实例的创建与初始化,相当于程序已启动,实例就创建好了;

:这里之所以将初始化也包括在类加载中是一种口头上的描述,类加载、连接、初始化这三个阶段进程是紧密关联的,所以这三个阶段的行为会被统称为“类加载的效果”,这样简化的说法却是为了方便理解,若更细化则应该按照类的生命周期来描述,故以下将这三个阶段统称为类加载

如果用代码来表示的话,需要符合三个要求:

  • 在类刚加载时就使用static关键字修饰并创建实例;
  • 不对外提供修改实例的方法,只提供返回唯一实例的方法;
  • 构造方法设为私有,这样new的话就会报错

:static修饰的静态变量在类加载过程中就会被JVM分配内存并初始化,并且只会被初始化一次,保证了全局唯一性

代码示例:

class Singleton {
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {}
}

可以看到,在类加载时就完成了对类的实例化,而这个实例也是这个类的唯一实例,获取该实例唯一的方法就是通过getInstance()调用,并且若打算通过new的方式创建新的实例,会报错:

在这里插入图片描述

多线程环境下

上述代码符合单例模式的要求,且不仅在单线程环境下如此,在多线程环境下也能使用,且它是线程安全的,因为饿汉模式并没有提供修改对象的操作,只有读操作(返回唯一实例的方法),故不会造成线程安全问题。

3. 懒汉模式

3.1 单线程环境

懒汉模式也是单例模式的一种实现方式,要求只能创建一个唯一的实例,但创建实例的时间却慢了很多,“懒”到在第一次使用的时候才创建实例,代码示例🌰如下:

class SingletonLazy {
    private static SingletonLazy instance = null;

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

    private SingletonLazy() {};

}

public class Test7 {

    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }

}

// 代码执行结果
true

从输出结果可见,在第一次调用getInstance()方法时来创建该实例,之后再调用这个方法时因为instance不为空所以不会再重复创建实例,而是直接返回这个唯一实例,这也是符合单例模式要求的实现方式!

3.2 多线程环境

上述代码在单线程的环境下是能够正常使用的,那在多线程环境下呢?

我们仔细观察,其实能够发现上述代码有一处地方对instance进行了修改操作:

instance = new  SingletonLazy();

如果在多线程环境下,这段代码可能就会导致线程安全问题的发生!

假设有t1、t2两个线程,t1线程在校验后发现当前实例未被创建,于是进入了分支,可在这时t2线程也发现了实例未被创建,于是也进入了分支,并在t1线程创建实例之前就创建好了实例并赋值给了引用变量instance,此时实例已经被创建出来了,但t1却不知道,并继续创建了新实例,这就违背了单例模式的原则了!

上述情况如图所示:

在这里插入图片描述

那么如何改进懒汉模式让它成为线程安全的代码?依照之前学过的线程安全的知识,我们可以给这段代码加锁🔒,如synchronized,且加锁的时候也要加对位置才行,代码示例🌰如下:

class SingletonLazy {
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        synchronized (SingletonLazy.class) { // 锁对象设置为类对象,因为类对象也是唯一的
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }

    private SingletonLazy() {};

}

在加上锁后,上述因线程安全问题导致的实例重复创建问题就能够被解决了!

解决这个问题之后,我们却发现,加上锁后代码执行效率却下降了,明明我的synchronized只需要在第一次创建实例时加锁就可以了,后面的instance都不为空,就不需要加锁来阻塞线程执行了!

那么,又有什么方法来提高效率呢?其实很简单,只要再给锁的外围加锁一个条件判断来校验即可:

class SingletonLazy {
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy() {};

}

外围也加上条件判断后,我们的锁只会在第一次创建实例的时候被调用,这样我们的代码执行效率得到提高了!此时我们的锁,也被称为双重校验锁!!(双重校验 + 一个锁)

上面出现了两个条件判断语句,它们具体作用如下

  • 第一个出现的if:用来提高代码执行效率,减少锁的重复调用;
  • 第二个出现的if:用来解决线程安全问题,防止实例的重复创建;

3.3 指令重排序引发的线程安全问题

指令重排序也是编译器用来优化我们代码的一种方式,它能够调整代码原有的执行顺序,在不改变代码逻辑的前提下来提高程序的执行效率。但在多线程的环境下,可能会给我们带来线程安全问题!

就上述代码所示,其中有一行代码:

 instance = new SingletonLazy();

它的执行可以细分为三个步骤:

  1. 申请一段内存空间
  2. 在刚刚申请的内存空间上,通过调用构造方法创建实例对象
  3. 将内存地址赋值给引用变量instance

上述的三个步骤,如果在多线程的环境下,因为线程的随机调度以及编译器的指令重排序优化,可能会出现下述的问题:

在这里插入图片描述

此时由t2返回的instance是一个非空但未被初始化的对象,这个对象的属性此时都是未初始化的全为"0"的值,如果这个时候调用了这个对象的属性或方法,可能就会引起代码的逻辑问题!

那么,指令重排序引起的线程安全问题又该怎么解决?请出我们的老伙计:volatile!!

volatile不仅能用来解决内存可见性问题,也能用来解决指令重排序问题,它能够禁止指令重排序,被volatile修饰的变量的读写操作是不会被重排序的,进而解决指令重排序带来的线程安全问题!

:volatile修饰的变量,每次在被访问使用时都要重新读取内存,而不会被优化到寄存器或缓存中,进而解决内存可见性引起的线程安全问题

代码示例🌰:

class SingletonLazy {
    private volatile static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (SingletonLazy.class) { // 锁对象设置为类对象,因为类对象也是唯一的
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy() {};
}

以上便是对单例模式的介绍了,如果这些内容对大家有帮助的话请给一个三连关注吧💕( •̀ ω •́ )✧( •̀ ω •́ )✧✨

;