前言
什么是单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。
这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。
这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
# 摘自菜鸟教程:https://www.runoob.com/design-pattern/singleton-pattern.html
单例模式又分 饿汉模式,懒汉模式本片文章主要讲解懒汉模式
懒汉模式
- 首先来看一下它的定义
懒汉模式:延迟加载,只有在真正使用的时候,才开始实例化.
实现方式
1.双检锁
private static volatile LazySingleton instance;
private LazySingleton(){
if(instance != null){
throw new RuntimeException("不允许通过反射获取");
}
}
public static LazySingleton getInstance() {
//第一次检查
if (instance == null) {
//获取锁
//第一次访问,多个线程同时挤进来,只有一个线程可以获取锁
synchronized (LazySingleton.class){
//第一个线程进入 此处为空,进入if并创建对象并返回,之后获得锁的线程此处判断不为空直接返回
if(instance == null) {
//执行构造方法
instance = new LazySingleton();
}
}
}
return instance;
}
问题1:此处为什么使用volatile.private static volatile LazySingleton instance;
创建对象是非原子性操作,有三个过程 分配空间,初始化对象,赋值,其中2,3步其中可能会发生指令重排现象.
-
代码示例
//假设我们有如下语句 Holder holder = new Holder(); //则实际执行的操作如下 tmpRef=allocate(Holder.class)//1.分配空间 invokeConstructor(tmpRef)//2. 执行构造函数 holder = tmpRef //3.赋值
- 只有在初始化对象的那一步才会真正执行构造方法
- 编译器(JIT),CPU有可能对指令进行重排序,导致使用到尚未初始化的实例,可以通过添加volatile关键字进行修饰,对于volatile修饰的字段,可以防止指令重排.
问题2:构造方法的反射判断
对于构造方法声明为private,可以防止直接new对象,但是阻止不了反射来获取对象,从而破坏单例.
private LazySingleton(){
if(instance != null){
throw new RuntimeException("不允许通过反射获取");
}
}
以上代码并不能完美的阻止反射,如果从一开始就直接使用反射而不直接去调用提供的创建方法 就会被破解
- 代码示例
Class<LazySingleton> lazySingletonClass = LazySingleton.class;
Constructor<LazySingleton> constructor = lazySingletonClass.getDeclaredConstructor();
// 暴力反射
constructor.setAccessible(true);
// 从一开始就不使用给定的方法来创建单例
//LazySingleton instance = LazySingleton.getInstance();
LazySingleton lazySingleton = constructor.newInstance();
LazySingleton lazySingleton1 = constructor.newInstance();
System.out.println(lazySingleton);
System.out.println(lazySingleton1);
执行结果
com.leetao.singleton.LazySingleton@511d50c0
com.leetao.singleton.LazySingleton@60e53b93
完全是两个对象...
- 难道就真的任反射随意宰割了? 别着急,下面会通过静态内部类的方式来介绍如何解决的
- 在这之前,还是先得来了解一下序列化破坏反射吧
序列化破坏
- 代码实例
//序列化的对象已实现Serializable接口
//内存输出流,此处也可以使用文件输出流(持久化)来代替
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//内存输入流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
//获取单例
LazySingleton instance = LazySingleton.getInstance();
//存入对象输出中
objectOutputStream.writeObject(instance);
objectOutputStream.flush();
objectOutputStream.close();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
//对象输入流
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
LazySingleton lazySingleton = ((LazySingleton) objectInputStream.readObject());
//方法创建的单例
System.out.println(instance);
//序列化之后的单例
System.out.println(lazySingleton);
运行结果
com.leetao.singleton.LazySingleton@6f94fa3e
com.leetao.singleton.LazySingleton@4e50df2e
也不是同一个...
- 关于序列化对象之后为什么不是同一个的问题
- 因为使用了默认的序列化机制,他会直接从字节流中拿数据,并不会去调构造函数来进行初始化
附1:JAVA序列化过程
1.将对象实例相关的类元数据输出。
2.递归地输出类的超类描述直到不再有超类。
3.类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
4.从上至下递归输出实例的数据
脚注: 默认的序列化机制会将对象所有实现Serializable接口的-内容-全部序列化,序列化过程会读取内容的字节流数据,会通过此产生新的对象,并不是通过构造函数来制造新的对象(网上很多文章都说序列化通过构造函数来创建对象,其实并不是!!)。
原型模式中的深克隆也就是通过此机制来实现的.
脚注所示内容代表的是:对象继承的类以及超类..、成员变量中的引用变量
-
那么单例中的序列化破坏如何解决
官方答案
- 可以通过写入readResovler() throws ObjectStreamException 方法来实现自定义的序列化机制
代码
/**
* 自定义的序列化机制
*/
private Object readResolve() throws ObjectStreamException{
//直接返回单例
return LazySingleton.getInstance();
}
再次运行结果
com.leetao.singleton.LazySingleton@6f94fa3e
com.leetao.singleton.LazySingleton@6f94fa3e
序列化破坏的问题完美解决
2. 静态内部类
1.本质上是利用类加载器机制来保证线程安全
2.只有在实际使用的时候,才会触发类的初始化,所以也是懒加载的一种形式
3.借助于jvm类加载机制,保证实例的唯一性.
何为类加载
# 类加载过程
1. 加载: 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象)
2. 连接: a.校验,b.准备(给类的静态成员变量赋默认值),c.解析
3. 初始化:给类的静态变量赋初值
# 注意:只有在真正使用对应的类是,才会触发初始化 如
1. 当前类是启动类(main方法所在的类).
2. 直接进行new操作.
3. 访问静态字段(final修饰的静态字面量除外).
4. 访问静态方法.
5. 用反射访问类.
6. 初始化此类的子类.
$. 后续会出关于类加载相关的文章
前面提到反射破坏的问题,在静态内部类中可以这样解决
public class LeeFactory {
private LeeFactory(){
//通过静态类加载机制破解反射破坏
if(LeeFactoryHolder.LEE_FACTORY!=null){
throw new UnsupportedOperationException("非法反射不予允许");
}
}
/**
* 静态内部类
*/
private static class LeeFactoryHolder{
private static final LeeFactory LEE_FACTORY = new LeeFactory();
}
public static LeeFactory getInstance(){
return LeeFactoryHolder.LEE_FACTORY;
}
}
以上代码可以完美解决反射破坏,如果直接通过getInstance()的方式来获取对象的话.第一次调用才会触发类初始化和构造方法.之后的调用直接拿数据
而第一次调用反射会触发两次两次构造方法,
1.构造方法中if中的判断会调用一次(因为访问类字段会涉及到类初始化,类初始化调用了构造方法)
2.反射创建对象本身会调用一次构造方法,此时静态内部类字段因为初始化已经存在值了(判断有值,抛出异常)
还有一个特点
private static final LeeFactory LEE_FACTORY = new LeeFactory();
为什么加final
1.因为一旦被赋值便无法在修改,即使是反射也不能(也是因为这个原因才使用final)
2.被final修饰的字段会在完全初始化后才会对其他线程可见
说到这里不得不说为什么不用volatile
final和volatile
volatile修饰的字段不但可以防止重排序,还可以直接在主存更新,让其他线程同步更新
但是它阻止不了反射对其重新赋值,如果使用反射对内部类字段赋值为null,会导致其他正常调用的代码出现问题.
而final修饰的类,会在完全初始化后才会对其他线程可见,而且不能被反射破坏,正好符合我们的需求
如果对final感兴趣的同学,可以阅读
https://zhuanlan.zhihu.com/p/100536345 了解更多
结尾:浅谈枚举单例
枚举本质上是一个不可变类 ,它的成员全部为类字段.它不可以被反射所破坏,同时还拥有自己的序列化机制.可以说是完美的单例.
参考
Java序列化机制https://www.iteye.com/blog/bingobird-867950
final特征 https://zhuanlan.zhihu.com/p/100536345
文中所述内容,如有错误,欢迎指正.
忌妒别人,不会给自己增加任何的好处,忌妒别人,也不可能减少别人的成就。
菅江晖