单例模式¶
单例模式的核心目的就是控制对象的创建,以达到特定对象的唯一性,实际业务中有很多都是要保证对象的不变性, 其中Spring Boot框架的容器思想,将对象放入容器进行管理,这个对象就是单例对象。 具体实现上,最经典的就是懒汉和饿汉两种,懒汉就是你需要使用到了才创建,饿汉就是无论你用不用的到都先创建。 这其实很具有计算机的思想,在很多其他地方都有应用。
➊ 懒汉式¶
懒汉式就是使用到了固定对象在创建它,按照我的理解就是避免创建了对象,却长时间不使用而造成计算机资源的浪费, 但是代价就是,你要用就要自己创建,这个是要时间的,尽管在现代计算机这个时间很短。而且这个方式有一个最大的问题: 使用时要考虑多线程环境,不然不安全,会创建多个对象而破坏了对象的唯一性。
要控制对象的创建,第一步就是将构造方法设置成private,否则没有任何一种方式叫作单例。第二步就是要有一个获取对象的方法, 且是public的,示例代码中的getInstance()就是,通过判断对象是否已经存在,来保证对象的唯一性。
但是它在多线程环境下是不安全的,多个线程同时调用getInstance()方法,if判断这个条件是不安全的,也就容易破坏单一性,适用方法就有限制。
public class SingletonOfLazy {
private static SingletonOfLazy INSTANCE = null;
private SingletonOfLazy() {
if (INSTANCE != null) {
throw new RuntimeException("单例对象无法重复创建");
}
}
public static SingletonOfLazy getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingletonOfLazy();
}
return INSTANCE;
}
}
➋ 饿汉式¶
饿汉式就是无论你用不用的到,我都先创建对象,你要用就直接用,比喻使用者饿了,不用等待创建过程直接“吃”。
首先它在类加载时就创建实例,同时Java的类加载机制确保了类是线程安全的;
然后私有构造函数,防止外部实例化;
最后提供全局访问点以获取对象。
public class SingletonOfHungry {
private static final SingletonOfHungry INSTANCE = new SingletonOfHungry();
private SingletonOfHungry() {
if (INSTANCE != null) {
throw new RuntimeException("单例对象无法重复创建");
}
}
public static SingletonOfHungry getInstance() {
return INSTANCE;
}
}
➌ 双检锁懒汉式¶
前面两种,一种懒汉式,用到才创建,避免了对象创建而长时间不使用的问题,但是它有线程安全问题; 一种饿汉式,直接创建对象,将创建对象的资源消耗放到类加载时,也避免了线程安全问题,但是对象一直在内存里而不使用, 会有浪费。两者各有优劣。
双检锁懒汉式是正对懒汉式线程安全问题的一种改良,很好的体现了对于锁🔒的理解。使用起来很简单,理解里面的意思就难了, 比如两次判断的含义,也算是面试会考的题目了。
public class SingletonOfDCLLazy {
/**
* 用volatile主要是为了保证变量的有序性,因为CPU会自己优化代码执行顺序,
* 单线程下无所谓,多线程下会有问题,而volatile可以防止指令重排,它会在赋值操作后加上一个内存屏障,
* 这样就可以让之前的操作不允许越过这个赋值操作来执行,而防止CPU的自动优化.
*/
private static volatile SingletonOfDCLLazy INSTANCE = null;
private SingletonOfDCLLazy() {
//构造器必须私有 不然直接new就可以创建
}
public static SingletonOfDCLLazy getInstance() {
// 1️⃣第一次判断,假设会有好多线程,如果doubleLock没有被实例化,那么就会到下一步获取锁,只有一个能获取到,
// 如果已经实例化,那么直接返回了,减少除了初始化时之外的所有锁获取等待过程
if (INSTANCE == null) {
synchronized (SingletonOfDCLLazy.class) {
// 2️⃣第二次判断是因为假设有两个线程A、B,两个同时通过了第一个if,然后A获取了锁,进入,然后判断doubleLock是null,
// 他就实例化了doubleLock,然后他出了锁,
// 这时候线程B经过等待A释放的锁,B获取锁了,如果没有第二个判断,那么他还是会去new DoubleLock(),
// 再创建一个实例,所以为了防止这种情况,需要第二次判断
if (INSTANCE == null) {
// 下面这句代码其实分为三步:
// 1.开辟内存分配给这个对象
// 2.初始化对象
// 3.将内存地址赋给虚拟机栈内存中的doubleLock变量
// 注意上面这三步,第2步和第3步的顺序是随机的,这是计算机指令重排序的问题
// 假设有两个线程,其中一个线程执行下面这行代码,如果第三步先执行了,就会把没有初始化的内存赋值给doubleLock
// 然后恰好这时候有另一个线程执行了第一个判断if(doubleLock == null),然后就会发现doubleLock指向了一个内存地址
// 这另一个线程就直接返回了这个没有初始化的内存,所以要防止第2步和第3步重排序
INSTANCE = new SingletonOfDCLLazy();
}
}
}
return INSTANCE;
}
}
➍ 内部类单例¶
这个模式使自己比较喜欢的,没有那么多弯弯绕,利用Java的静态内部类机制来实现懒加载和线程安全,用起来也方便。
public class SingletonOfInnerClass {
private SingletonOfInnerClass() {
}
private static class Holder {
static SingletonOfInnerClass INSTANCE = new SingletonOfInnerClass();
}
public static SingletonOfInnerClass getInstance() {
return Holder.INSTANCE;
}
}
➎ 枚举单例¶
Java的枚举类型天然支持单例模式,并且是线程安全的,就是使用范围小,只能说算是,但是你要写一个单例, 枚举肯定不在考虑范围,枚举的设计就不是为单例而生,只是“凑巧”它天然是单例。