JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了

一、前言 单例模式属于创建型模式,保证了单例类在系统中仅存在一个实例。能够避免频繁创建某个对象,在一定程度上可以减少内存占用。
本文会讲解单例类的多种实现种类,并从源码层面说明保证线程安全、反射安全与序列化安全的措施。
二、单例模式的实现种类 饿汉式

public class Singleton {private static Singleton instance = new Singleton(); private Singleton() { }public static Singleton getInstance() { return instance; }}

优点:
得益于类加载机制(关于类加载机制,可以参考我的另外一篇文章new一个对象的背后,竟然有这么多可以说的),在初始化时就会加锁执行所有的静态方法,直接避免了在使用时的多线程同步问题
缺点:
无论当前类的实例什么时候用,都会在正式使用前创建实例对象。
如果我们依赖的所有外部jar中都使用此模式的话,就会造成大量实例提前贮存在内存中。而我们可能从始到终都用不到该实例对象,从而在一定程度上造成内存的浪费。
懒汉式 解决饿汉式浪费内存的问题
public class Singleton {private static Singleton instance; private Singleton() { }public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}

优点:
懒加载,只在需要的时候才实例化对象,是一种牺牲时间换取空间的策略,能有效解决饿汉式浪费内存的问题。
对于懒加载,在之前写的SpringBoot的自动装配原理、自定义starter与spi机制,一网打尽中,JDK中的spi机制也使用了懒加载模式,ServiceLoader内部会借助一个LazyIterator,而LazyIterator实现了Iterator,其hasNext()方法会去寻找下一个服务实现类,调用next()方法才会利用反射实例化该实现类,起到一种懒加载的作用。
缺点:
【JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了】线程不安全,即多线程情况下,容易被多个线程实例化出多个对象,违背”单例“的原则
线程安全的懒汉式(非DCL) 解决懒汉式线程不安全的问题
public class Singleton {private static Singleton instance; private Singleton() { }public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}

优点:
实现简单,直接给getInstance()方法加上同步锁。
缺点:
锁的粒度很粗,在创建完成后,多个线程同时获取单例对象是不需要加锁的,因此非DCL模式性能较低。
线程安全的懒汉式(DCL) 降低非DCL模式的锁的粒度
public class SingletonDCL {private volatile static SingletonDCL instance; private SingletonDCL() { }public static SingletonDCL getInstance() { if (instance == null) { synchronized (SingletonDCL.class) { if (instance == null) { instance = new SingletonDCL(); } } } return instance; }}

可能初学的读者有以下的疑问:
为什么要检验两次是否为null?
最初的想法,就是非DCL模式的例子,但那样效率太低,我们应当缩小锁的范围。
在单例模式下,要的就是一个单例,new SingletonDCL()只能被执行一次。因此,现在初步考虑成以下的这种方式:
public static SingletonDCL getInstance() { if (instance == null) { synchronized (SingletonDCL.class) { //一些耗时的操作 instance = new SingletonDCL(); } } return instance; }


但这样,存在一个问题。线程1与线程2同时判断instance为null后,接着线程1拿到锁了,创建了单例对象并释放锁。线程2拿到锁之后,又创建了单例对象。
此时线程1和线程2拿到了两个不同的对象,违背了单例的原则。
因此,在获取锁之后,需要再进行一次null检验。

为什么使用volatile 修饰单例变量?
这段代码,instance = new SingletonDCL(),在虚拟机层面,其实分为了3个指令:
  • 为instance分配内存空间,相当于堆中开辟出来一段空间
  • 实例化instance,相当于在上一步开辟出来的空间上,放置实例化好的SingletonDCL对象,各项实例变量已经初始化好并且被赋予指定值
  • 将instance变量引用指向第一步开辟出来的空间的首地址
但由于虚拟机做出的某些优化,可能会导致指令重排序,由1->2->3变成1->3->2。这种重新排序在单线程下不会有任何问题,但在多线程的情况下,可能会出现以下的问题:
线程1获取锁之后,执行到了instance = new SingletonDCL()。此时,由于虚拟机进行了指令重排序,先进行了第1步开辟内存空间,然后执行了第3步,instance指向空间首地址,第2步还没来得及执行,此时恰好有线程2执行getInstance方法,最外层判断instance不为null(instance已经指向了某一段地址,因此不为null),直接返回了单例对象,这个时候,线程2就拿到了一个不完整的单例对象。
因此这里使用volatile修饰单例变量,来避免指令重排序。对于volatile关键字的原理分析,会另开篇幅。
静态内部类 不需要使用手动加锁的懒加载模式
public class Singleton {private Singleton() { }private static class SingletonHolder { private static final Singleton instance = new Singleton(); }public static Singleton getInstance() { return SingletonHolder.instance; }}

优点:
当仅调用外部类的一些属性时,只会加载外部类,并不会加载内部类(不管是静态还是非静态内部类)。只有在显示使用内部类的属性时,才会去加载内部类。
也即是说,仅使用Singleton类时,不会去加载SingletonHolder内部类,也更不会去初始化SingletonHolder内部类中的instance 变量,起到一种懒加载的作用。
当使用到单例对象时,静态属性又利用到了类加载机制,保证了线程安全。
另外值得注意的是,直接使用静态内部类的属性时,也会去加载外部类,但静态内部类实际上并不依赖外部类。
当使用非静态内部类时,则需要先创建一个外部类对象。因为非静态内部类会隐式持有外部类的一个强引用,体现在构造函数需要传入外部类对象。也就是说,非静态内部类依赖外部类。
枚举 单例的最佳实践,也是《Effective Java》作者Josh Bloch提倡的方式
public enum EnumSingleton { //模拟单例中的数据 INSTANCE(new Object()); private Object data; EnumSingleton(Object data) { this.data = https://www.it610.com/article/data; }public Object getData() { return data; } }

外部类中调用EnumSingleton.INSTANCE即可获取到单例对象
直接看是看不出什么门道的,利用javac EnumSingleton.java编译文件,再利用javap -p EnumSingleton.class查看反编译后的内容
Compiled from "EnumSingleton.java" public final class com.yang.ym.testSingleton.EnumSingleton extends java.lang.Enum { public static final com.yang.ym.testSingleton.EnumSingleton INSTANCE; private java.lang.Object data; private static final com.yang.ym.testSingleton.EnumSingleton[] $VALUES; public static com.yang.ym.testSingleton.EnumSingleton[] values(); public static com.yang.ym.testSingleton.EnumSingleton valueOf(java.lang.String); private com.yang.ym.testSingleton.EnumSingleton(java.lang.Object); public java.lang.Object getData(); static {}; }

看不到方法体,我们换xjad工具,下载地址http://files.blogjava.net/96sd2/XJad2.2.rar
得到反编译后的内容如下:
public final class EnumSingleton extends Enum { public static final EnumSingleton INSTANCE; private Object data; private static final EnumSingleton $VALUES[]; public static EnumSingleton[] values() { return (EnumSingleton[])$VALUES.clone(); } public static EnumSingleton valueOf(String s) { return (EnumSingleton)Enum.valueOf(EnumSingleton, s); } private EnumSingleton(String s, int i, Object obj) { super(s, i); data = https://www.it610.com/article/obj; } public Object getData() { return data; } static { INSTANCE = new EnumSingleton("INSTANCE", 0, new Object()); $VALUES = (new EnumSingleton[] { INSTANCE }); } }

可以看到,在编译完枚举类EnumSingleton后,会生成一个构造函数,静态代码块中使用构造函数对枚举项进行实例化。
在加载EnumSingleton枚举类时,就会在初始化阶段触发静态代码块的执行,因此枚举类是线程安全的、非懒加载模式。
三、破坏单例模式 对于单例模式,一个好的实现方式,应当尽量保证线程安全、反射安全与序列化安全。
对于线程安全,指的是多个线程下,只有一个线程能创建单例对象,且所有线程只能获取到同一个完整的单例对象。
对于反射安全,指的是无法利用反射机制去突破私有构造器,从而避免产生多个对象。
对于序列化安全,指的是无法通过反序列生成一个新对象。

利用反射机制破坏单例 反射破坏饿汉式
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { //获取无参的私有构造器 Constructor constructor = Singleton.class.getDeclaredConstructor(); //设置可以访问私有变量 constructor.setAccessible(true); //使用私有构造器实例化对象 Singleton singleton = constructor.newInstance(); System.out.println(singleton == Singleton.getInstance()); //false }

这个时候,使用反射就轻而易举地创建了新对象,违反了单例的原则。

饿汉式保证反射安全
饿汉式在类加载时,就会创建出单例对象,一旦单例对象不为空,构造方法直接抛出异常即可。
改造后的饿汉式如下:
public class Singleton {private static Singleton instance = new Singleton(); private Singleton() { if (instance != null) { throw new RuntimeException("can not create singleton"); } }public static Singleton getInstance() { return instance; }}

在使用反射之后,会抛出异常,拒绝创建新的对象。

静态内部类保证反射安全
其实也是同样的修改方式:
public class Singleton { private Singleton() { if (SingletonHolder.instance != null) { throw new RuntimeException("can not create singleton"); } }private static class SingletonHolder { private static final Singleton instance = new Singleton(); }public static Singleton getInstance() { return SingletonHolder.instance; }}

当使用反射调用构造器时,进行判空时,就会触发内部类的加载,从而instance不为空,抛出异常。
正常使用Singleton.getInstance()时,触发内部类的加载,也会进入到构造方法中,但此时已经加载完内部类,因此instance仍旧为空,能够进行实例化。

懒汉式保证反射安全的思考
对于懒汉式,如果也是这样做的话,是无法保证反射安全的。具体的解决方案,我也没什么思路,有知道的小伙伴可以在下方留言。

反射破坏枚举类
从上一节反编译枚举类可知,EnumSingleton是没有无参的构造函数的,不过有一个有参构造函数,那么我们修改一下代码:
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { //获取有参的私有构造器 Constructor constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class, Object.class); //设置可以访问私有变量 constructor.setAccessible(true); //使用私有构造器实例化对象 EnumSingleton singleton = constructor.newInstance(); EnumSingleton instance = EnumSingleton.INSTANCE; System.out.println(singleton == instance); }

运行就直接报错了
JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了
文章图片

提示newInstance这一行抛出了异常,进入到该方法中
JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了
文章图片

如果当前是枚举类型时,直接抛出异常。由此看来,枚举具有天然的反射安全性质。

利用序列化机制破坏单例 当把一个对象序列化到文本中,再从文本中反序列化后,可能反序列化后得到对象会被重新分配内存,也就是说,会新创建一个对象。
序列化破坏非枚举
值得注意的是,饿汉式需要先实现Serializable接口。
public static void main(String[] args) throws IOException, ClassNotFoundException { Singleton instance = Singleton.getInstance(); ObjectOutputStream ops = new ObjectOutputStream(new FileOutputStream("enum.txt")); ops.writeObject(instance); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("enum.txt")); Object object = ois.readObject(); System.out.println(instance == object); //false }

看来,反序列化后,确实得到了一个新的对象。
我们进入到readObject中,看看内部做了什么处理
JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了
文章图片

核心方法是 readObject0,当反序列化一个TC_OBJECT(这个标识会在writeObject时写入到文本的开头),会调用readOrdinaryObject
JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了
文章图片

接着进入到readOrdinaryObject中
JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了
文章图片

isInstantiable是检查类是否可以被实例化,当前肯定是支持的,因此会使用newInstance反射创建对象。
值得注意的是,newInstance调用的并不是单例类的构造方法,而是Object的,因此会在接下来拿文本中的数据填充当前得到的Object。
对饿汉式、懒汉式或静态内部类而言,序列化后会创建新的对象,从而破坏了单例模式。
那么,有什么方法避免呢?

非枚举保证序列化安全
其实答案就藏在isInstantiable的下方
JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了
文章图片

如果当前单例类有readResolve方法,就会进入到invokeReadResolve方法中,并将其返回的对象作为最终的readObject返回的对象。
JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了
文章图片

该方法返回的对象,就是执行readResolve方法返回的对象。
直接在单例类中添加readResolve方法,返回当前对象或者静态内部类中的对象即可。
public Object readResolve() { return instance; }

readResolve方法让类可以替换从流中读取到的对象,自由控制反序列化得到的对象。

序列化破坏枚举
修改下测试方法,直接运行。
意外的是,直接返回了true,说明枚举类能够保证序列化安全。
先进入writeObject方法中,其内部核心方法是writeObject0
JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了
文章图片

之后对序列化的类型进行了判断
JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了
文章图片

进入到writeEnum中
JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了
文章图片

发现,最终会往文本中写入类型、当前枚举类的全限定名、serialVersionUID版本号以及枚举项的name。
writeObject到这里就结束了,现在看readObject方法,核心方法是readObject0
先从文本中获取到类型,然后分别进行处理
JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了
文章图片

进入到readEnum中
JAVA|单例模式的6大种类,如何保证线程安全、反射安全以及序列化安全,这次终于通透了
文章图片

发现是直接利用Enum.valueOf((Class)cl, name)来查找处于cl枚举类中名称为name的枚举项,也就是说,只是查找,并没有创建新的实例。
因此,枚举类又天然具有序列化安全的性质。

    推荐阅读