单例模式
关键字:
双重检验、禁止指令重排序、锁,反射、反序列化、枚举
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 步:
- 给对象分配内存空间
- 初始化对象
- 将内存地址 指向 对象
指令重排序会打乱这些步骤,所以创建一个对象并不是线程安全的。
可能先走其他线程现在第三步,当前线程判断不为空直接返回了,实际返回的是一个 null 。
所以需要用 volatile 禁止指令重排序。
枚举的实现
你以为这就完了么? 并没有,要找漏洞也其实还有。
就是可以通过 反射 , 打破这样的单例效果。
获取到构造器后, setAccessible(true) 设置允许方法私有方法,就可以无限访问 私有构造函数了。。。。
还可以通过 反序列化, 也可以 打破
readObject( ) 从文件里 反序列化的时候, 总是返回的一个新的对象。
所以, 有什么解决办法呢?
枚举就可以~ 枚举 天然的就是线程安全的, 就是单例的~
防反射、 防反序列化:
在用 反射 newInstance( ) 的时候, 会判断当前类是不是 枚举, 如果是,就直接抛异常了
在反序列化的时候, 会用 Enum 类的 valueOf( ) 方法,直接根据变量名,找到对应的枚举类。