本人博客
https://www.liyunxu.work/
单例模式详解
单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
单例模式分为懒汉模式,饿汉模式,静态内部类,还会了解Java中的Enum
类型。
1. 懒汉模式(Double Check)
懒汉模式就是需要用到的时候再去加载,我们只有调用getInstance()
的时候才会加载,先去判断instance
是否为空,(不为空直接返回),如果为空则该线程拿到锁,进去需要再次判断instance
是否为空(不为空直接返回),如果为空才去创建对象。
1. 那么为什么需要进行两次判断为空呢?(Double Check)
我们假设一种情况,首先instance
为空有两个线程(T1,T2)并发执行到了第九行,他们回去抢占这个锁,我们假设T1抢到了锁进去到11行创建了对象,当T1释放锁,T2拿到锁进来的时候如果不进行里面的判断是否为空,那么T2也会执行第11行去创建对象,这样两个线程就拿到了两个对象,不符合单例模式全局唯一的原则了。
2. 第三行为什么要加volatile
关键字?
- 我们在字节码层理解一下Java创建对象的流程
- 分配空间
- 对空间进行初始化
- 引用赋值(堆地址的引用放在栈顶,并给
instance
赋值)
如果是正常的流程不加volatile
没有问题,但是我们的JIT(即时编译)和CPU都有可能对指令进行重排序 第二步和第三步可能会进行交换就会在多线程中出现问题。还是instance
为空有两个线程(T1,T2)T1先判断为空然后到第11行创建对象假设T1刚进行到第二步也就是引用赋值(第二步和第三步可能会进行交换后),还未初始化,我们第二个线程T2进来到第8行发现instance
不为空直接返回了一个未进行空间初始化的引用,可能就会导致空指针异常。我们加了volatile
关键字就会防止JIT(即时编译)和CPU对指令进行重排序。
public class LazySingleton {
// volatile
private volatile static LazySingleton instance;
// 构造函数私有化 不让外面去创建对象
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
/***
* 在字节码层
* JIT, CPU
* 1.分配空间
* 2。初始化
* 3 引用赋值
* JIT, CPU,可以会对指令进行重排序 第二部和第三部可以交换
* 所以要加volatile关键字防止进行优化
*/
}
return instance;
}
}
return instance;
}
}
2. 饿汉模式
本质上是借助了jvm类加载的机制,保证了实例的唯一性。
类加载过程
- 加载二进制数据到内存中,生成对应的Class模板
- 连接: 验证, 准备(给静态成员赋默认值),解析
- 初始化:给类的静态成员变量赋初值
只有真正使用对应类的时候才会进行初始化(比如在main的启动类,new对象的时候,访问静态属性, 访问静态方法,使用反射访问类,初始化一个类的子类)。
public class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return instance;
}
}
3. 静态内部类
这个实现的原理和上面的饿汉模式原理一样都是借助了jvm类加载的机制,保证了实例的唯一性。
但是静态内部类有一个好处就是可以实现懒加载,只有我们去调用getInstance()
的时候才去加载这个实例。
public class InnerClassSingleton {
private static class InnerClassHolder {
//可以实现懒加载
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton() {}
public static InnerClassSingleton getInstance() {
return InnerClassHolder.instance;
}
}
4. 枚举
枚举本质上就是类里面的静态属性,所以枚举天然就是单例的
并且枚举不允许通过发射去创建新的对象(上面的单例模式都是反射可以攻击的)会在newInstance
源码中抛出下面的异常。
注意下面这个16384 这个在jdk8的版本应该是ENUM
所以会抛出异常
if ((this.clazz.getModifiers() & 16384) != 0) {
throw new IllegalArgumentException("Cannot reflectively create enum objects");
}
public enum EnumSingleton {
INSTANCE;
public void print() {
System.out.println(this.hashCode());
}
}
class EnumSingletonTest {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// EnumSingleton instance = EnumSingleton.INSTANCE;
//会报错 不允许破坏单利
EnumSingleton instance1 = EnumSingleton.INSTANCE;
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);// 拿到枚举的构造方法
declaredConstructor.setAccessible(true); //设置忽略访问权限
EnumSingleton instance = declaredConstructor.newInstance("INSTANCE", 0);
System.out.println(instance == instance1);
}
}
5. 防止反射攻击
我们以静态内部类的方式举例,因为反射也是通过拿到一个类的构造方法去创建对象的他可以不管构造方法的权限修饰符所以我们只需要在构造方法中添加一个判断如过instance
不为空我们就不允许创建对象,直接抛出异常阻止创建对象。
public class InnerClassSingleton {
private static class InnerClassHolder {
//可以实现懒加载
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton() {
//在这里加个判断
if (InnerClassHolder.instance != null) {
throw new RuntimeException("单例不允许有多个实例");
}
}
public static InnerClassSingleton getInstance() {
return InnerClassHolder.instance;
}
}
6. 序列化单例对象的问题
如果一个对象要想支持序列化就要实现一下Serializable
接口他只起到标识作用,但如果我们只是实现了这个接口通过下面的方法可以测试出来,序列化进去的对象和反序列化出来的对象就不是同一个对象了,这就破坏了单例。
public class InnerClassSingleton implements Serializable {
private static class InnerClassHolder {
//可以实现懒加载
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton() {
if (InnerClassHolder.instance != null) {
throw new RuntimeException("单例不允许有多个实例");
}
}
public static InnerClassSingleton getInstance() {
return InnerClassHolder.instance;
}
}
所以我们需要加上一个函数和一个静态常量就可以做到反序列化的时候也是同一个对象
//解决序列化破坏单例的问题
Object readResolve() throws ObjectStreamException {
return InnerClassHolder.instance;
}
我们先看这个函数,就是告诉我们在反序列化的时候去哪找这个对象我们要提供一个权限修饰符任意,返回值类型为Object
方法名为readResolve()
无参的方法,来返回我们唯一实例的对象。
//序列化版本号
private static final long serialVersionUID = 42L;
然后再来看这个版本号,版本号也会序列化到文件中,然后反序列化的时候就会就找相同的版本号的对象。如果不指定这个版本号在我们序列化和反序列化过程中没有修改过这个对象也是没有问题的,如果我们序列化完成给对象添加了一个字段,然后在反序列化的时候就会创建新的对象。所以我们要指定版本号,那么及时我们修改了对象,反序列化的时候只要版本号没变就是一个对象,不会新建对象。
public class InnerClassSingleton implements Serializable {
//序列化版本号
private static final long serialVersionUID = 42L;
private static class InnerClassHolder {
//可以实现懒加载
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton() {
if (InnerClassHolder.instance != null) {
throw new RuntimeException("单例不允许有多个实例");
}
}
public static InnerClassSingleton getInstance() {
return InnerClassHolder.instance;
}
//解决序列化破坏单例的问题
Object readResolve() throws ObjectStreamException {
return InnerClassHolder.instance;
}
}
测试方法
public class Test {
public static void main(String[] args) throws Exception {
//序列化破坏单例模式
InnerClassSingleton instance = InnerClassSingleton.getInstance();
//序列化到一个文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable"));
oos.writeObject(instance);
oos.close();
//在文件中读取进行反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));
InnerClassSingleton o = (InnerClassSingleton) ois.readObject();
System.out.println(o == instance);
}
}