- 为什么静态内部类的单例模式是最推荐的?
- 如何在反射的情况下保证单例
- 如何在反序列化中保证单例
为了回答上面三个问题,下面将逐步进行分析
1. 饿汉式——线程安全的单例模式
1 | public class Singleton1 { |
优点:线程安全;缺点:类加载的时候已经实例化,浪费空间
2. 懒汉式
(1)懒汉式V1
1 | public class LazySingleton1() { |
线程不安全,给方法加锁
1 | public static synchronized LazySingleton1 getInstance() { |
由于将synchronized关键字加在方法上会造成线程阻塞(同步),在性能上会大打折扣。所以使用双重校验锁(在保证线程安全的同时提高了性能。)
1 | public static LazySingleton1 getInstance() { |
(2) 懒汉式V2
内部类加载机制
1 | public class OuterTest { |
说明:1. 加在一个类时,其内部类不会被同时加载。2. 一个类被加载,当且仅当某个静态成员(静态域,构造器,静态方法等)被调用时发生。
回到第一版的双重校验锁。其实不管性能如何优越,还是使用了synchronized关键字,那么对性能始终是有影响的(有兴趣的话可以了解一下synchronized的内存模型与底层原理)。所以,下面给出了改良版本——使用静态内部类的方式。
1 | public class LazySingleton2 { |
由于对象实例化是在内部类加载的时候构建的,因此是线程安全的(在方法中创建对象才存在并发问题,静态内部类随着方法的调用而被加载,只加载一次,不存在线程安全问题)。
在getInstance()方法中没有使用synchronized关键字,因此没有造成多余的性能损耗。当LazySingleton2类加载的时候,其静态内部类SingletonHolder并没有被加载,因此instance对象没有被构建。
而我们在调用LazySingleton2.getInstance()方法时,内部类SingletonHolder被加载,此时单例对象才被构建。因此,这种写法节约空间,达到了懒加载的目的。
(3) 懒汉式V3
使用静态内部类的懒加载方式虽然具有很多优良特性,但是在反射的作用下,单例结构还是会被破坏:
1 | public class LazySingleton2Test { |
由于上面的问题,所以诞生了第三版的懒汉式:
1 | public class LazySingleton3 { |
这就保证了反射无法破坏其单例性。
(4) 懒汉式V4
在分布式系统中,有些情况下需要在单例中实现Serializable接口。这样就可以在文件系统中存储它的状态并且在稍后的某一时间点取出。
将 public class LazySingleton3 修改为 public class LazySingleton3 implements Serializable
1 | public class LazySingleton3Test { |
显然,出现了两个实例类,说明破坏了单例模式。我们需要提供 readResolve() 方法的实现。readResolve()代替了从流中读取对象。这就确保了在序列化和反序列化的过程中没人可以创建新的实例。下面是懒汉式第四个版本:
1 | public class LazySingleton4 implements Serializable { |
总结
在项目开发中根据情况选择相应的版本使用即可,并没有强制要求使用哪个版本。当然,一般来说,第二个版本(懒汉式V2)是使用的比较多的。