单例模式

关键字:

双重检验、禁止指令重排序、锁,反射、反序列化、枚举

WHAT

单例模式是指, 对象在内存中只存在一份实例,每次获取对象的时候,都是拿到的同一个内存地址的对象。

WHY

使用单例模式,可以避免频繁创建、销毁对象这样的开销,直接获取到地址值就可以。 另一方面,有可能我们每次需要的都是同一个对象,也就说有这样的业务需求,就比如 Spring 里默认的 bean 都是 singleton 模式的。

HOW

大体来说有 2 种方式实现。

饿汉式

类加载的时候就是直接进行实例初始化: 饿汉式。

比如:

public class Singleton_hungry{
    
    // static : 在类加载过程的 准备阶段 , 进行内存分配并初始化。
    // 类加载过程: 加载、验证、准备、解析、初始化
    private static Singleton_hungry instance = new Singleton_hungry;
    
    // 私有构造, 不能实例化
    private Singleton_hungry(){
    } 
    
    public static Singleton_hungry getInstance(){
    
        return this.instance;
    }
}

懒汉式

到用到这个类的时候,才去看他有没有进行初始化: 懒汉式。

写最简陋的版本,会有些问题,比如线程安全问题,指令重排序问题。

这里直接给出 双重校验+锁+禁止指令重排序 的版本了:

public class Singleton_lazy{
    
    // volatile : 禁止指令重排序
    private volatile Singleton_lazy instance;
    
    // 私有构造, 不能实例化
    private Singleton_lazy(){
    }
    
    public Singleton_lazy getInstance(){
    
        if (instance == null) {
    		// 如果不为空,直接拿到内存地址 返回单例
            synchronized(Singleton_lazy.class){
     //可能A,B线程都进到此区域,加锁保证线程安全
                if (instance == null) {
     //双重检验:只有1个线程进来,只能初始化1次,保证单例
                    instance = new Singleton_lazy();
                }
            }
        }
        return instance;   // 这里需要 volatile,避免发生 空指针异常。 有重排序的时间差
    }
    
}

volatile: 可以保证 可见性和有序性, 可以禁止指令重排序, 通过内存屏障来实现。

一个对象的创建简单来说可以分为 3 步:

  1. 给对象分配内存空间
  2. 初始化对象
  3. 将内存地址 指向 对象

指令重排序会打乱这些步骤,所以创建一个对象并不是线程安全的。

可能先走其他线程现在第三步,当前线程判断不为空直接返回了,实际返回的是一个 null 。

所以需要用 volatile 禁止指令重排序。

枚举的实现

你以为这就完了么? 并没有,要找漏洞也其实还有。

就是可以通过 反射 , 打破这样的单例效果。

获取到构造器后, setAccessible(true) 设置允许方法私有方法,就可以无限访问 私有构造函数了。。。。

还可以通过 反序列化, 也可以 打破

readObject( ) 从文件里 反序列化的时候, 总是返回的一个新的对象。

所以, 有什么解决办法呢?

枚举就可以~ 枚举 天然的就是线程安全的, 就是单例的~

防反射、 防反序列化:

在用 反射 newInstance( ) 的时候, 会判断当前类是不是 枚举, 如果是,就直接抛异常了

在反序列化的时候, 会用 Enum 类的 valueOf( ) 方法,直接根据变量名,找到对应的枚举类。