🍰 单例模式
2022年6月20日
- 设计模式
🍰 单例模式
1. 概述
1) 是什么
- 保证一个类仅有一个实例,并提供一个访问它的全局访问点
2) 优点
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存)
- 避免对资源的多重占用(比如写文件操作)
- 省去了new操作符,降低了系统内存的使用频率,减轻GC压力
- 系统中某些类,如spring里的controller,控制着处理流程,如果该类可以创建多个的话,系统完全乱了
3) 缺点
2. 代码示例
1) 场景 & 目标
- 一个类的对象的产生是由类构造函数来完成的。如果一个类对外提供了public的构造方法,那么外界就可以任意创建该类的对象。
- 所以,如果想限制对象的产生,一个办法就是将构造函数变为私有的(至少是受保护的),使外面的类不能通过引用来产生对象。
- 没有 public setter(),外部类无法调用 setter() 创建该实例
- 同时为了保证类的可用性,就必须提供一个自己的对象以及访问这个对象的静态方法
- 提供一个 public static getter() 获取唯一的这个实例
2) 饿汉式
- 他想要用到这个实例的时候就能够立即拿到,而不需要任何等待时间
- 通过
static
的静态初始化方式,在该类第一次被加载的时候,就有一个SimpleSingleton
的实例被创建出来了。 - 由于该实例在类被加载的时候就创建出来了,所以也避免了线程安全问题 - 类加载过程中 JVM 保证该过程线程安全
- 适用场景:
- 在很多电商场景,如果这个数据是经常访问的热点数据,那我就可以在系统启动的时候使用饿汉模式提前加载(类似缓存的预热)
- 这样哪怕是第一个用户调用都不会存在创建开销,而且调用频繁也不存在内存浪费了
3) 懒汉式
- 适用场景:
- 用在不怎么热的地方,比如那个数据你不确定很长一段时间是不是有人会调用,那就用懒汉,
- 如果你使用了饿汉,但是过了几个月还没人调用,提前加载的类在内存中是有资源浪费的
volatile 发挥的作用
- 防止指令重排
- 保证可见性
instance = new Singleton()
分以下几个步骤:- 为 new Singleton() 分配内存
- 初始化
- 将 instance 指向这块内存
- 初始化 & 将 instance 指向这块内存可能顺序发生改变
- 此时若线程 A 先修改指针指向,初始化还未完成
- 线程 B 进入判断条件,发现
instance != null
直接得到该还未初始化完成的 instance 实例,并立即使用,就会发生错误
序列化的问题
- 序列化 & 反序列化是通过 ObjectOutputStream & ObjectInputStream 方法实现的,
- ObjectInputStream 的 readObject() 执行情况如下:
- readOrdinaryObject(boolean unshared)
public class ObjectInputStream extends InputStream implements ObjectInput, ObjectStreamConstants
{
public final Object readObject() throws IOException, ClassNotFoundException {
return readObject(Object.class);
}
private final Object readObject(Class<?> type) throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
if (! (type == Object.class || type == String.class))
throw new AssertionError("internal error");
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(type, false); // --> 继续跟入
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
// ...
try {
switch (tc) {
// ...
case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));
case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared)); // --> 继续跟入
// ...
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
private Object readOrdinaryObject(boolean unshared) throws IOException
{
// ...
Object obj;
try { // 正常情况下,会通过 反射 生成一个新的对象,因此会破坏单例模式
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
// ...
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()) // 如果有 readResolve() 方法
{
// 通过 反射 调用要被反序列化的类的 readResolve() 方法
// 因此,如果重写了 readResolve() 方法,在该方法中返回的是 原单例对象,那么这里获得的就是 原单例对象
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep); // 这里将 rep 赋值给 obj,然后返回 obj
}
}
return obj;
}
}
4) 静态内部类
- 也是通过类加载机制保证线程安全
5) 枚举
// 构造函数默认为 private,且只能为 private
public enum Singleton {
INSTANCE;
public Singleton getInstance() {
return INSTANCE;
}
}
枚举保证线程安全
- 定义一个枚举类
public enum t {
SPRING,SUMMER,AUTUMN,WINTER;
}
- 反编译
// T 为 final 修饰,不可被继承
public final class T extends Enum
{
private T(String s, int i)
{
super(s, i);
}
public static T[] values()
{
T at[];
int i;
T at1[];
System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
return at1;
}
public static T valueOf(String s)
{
return (T)Enum.valueOf(demo/T, s);
}
public static final T SPRING; // SPRING 属性被 static final 修饰,属于类静态变量 & 只能赋值一次
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T("SPRING", 0); // 类加载时被初始化进行赋值
SUMMER = new T("SUMMER", 1);
AUTUMN = new T("AUTUMN", 2);
WINTER = new T("WINTER", 3);
ENUM$VALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
- 解释:
- 假如说我们现在需要
SPRING
这个枚举对象 - 通过
SPRING = new T("SPRING", 0);
得到 - 可以看出
SPRING = new T("SPRING", 0);
首先是属于static
静态代码块的一部分,所以该属性会在类加载中被初始化,且线程安全,只加载一次 - 同时,
SPRING
属于T
类,而 T 对象类被final
修饰,表明该类不可被继承
枚举保证序列化安全
- Java规范要求:
- 在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,
- 反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象
- 且编译器是不允许任何对这种序列化机制的定制的
- 因此也不需要走
writeObject
、readObject
、readObjectNoData
、writeReplace
readResolve
等方法 - 从而也就不会通过反射生成新的实例了
- 所以说,枚举有自己的序列化 & 反序列化方式
3. 应用
1) Bean
- Spring依赖注入Bean实例默认是单例的
- 参见 框架下的循环依赖中的
getSingleton()
方法,里面通过 双重判断 + 锁 保证创建的是单例