单例模式的优点
单例对象的类保证只有一个实例存在。确保某个类有且仅有一个实例存在,避免产生多个对象消耗过多资源。
关键点
- 构造函数私有, 不对外开放
- 通过静态方法, 枚举或容器返回单例类对象
- 确保单例类对象有且仅有一个, 尤其在多线程环境下
- 确保单例类对象在反序列化时不会重新创建
实现方式:
饿汉式
在类初始化时就创建单例对象. 下面有两种实现方式:
静态对象单例
使用静态对象持有一个单例实例, 在类初始化时就创建这个单例对象.
|
|
枚举单例
利用枚举的单一性, 又能允许存在方法和属性的特性.
|
|
枚举单例相比静态对象的单例, 优点在于可以防止反序列化的时候创建新的对象.
饿汉式优点:
- 简单
- 线程安全(枚举单例也是)
饿汉式缺点:
- 如果实例的构造较耗时, 会造成类的加载过程较耗时
- 如果这个单例对象不会用到, 它也会一直存在, 造成内存的浪费
反序列化破解单例
当类实现了 Serializable
接口时, 类的对象就能被序列化和反序列化.但是当反序列化时, 获得的对象是重新分配的内存, 单例也就不存在了.
|
|
上面的例子就通过反序列化创建了一个新的对象, 绕过了单例的限制. 但是也有防止单例对象被反序列化的方法, 就是通过实现 readResolve
方法, 让该方法返回已有的单例对象.
|
|
可以看到 readResolve
在从外部流创建对象时会被调用, 能有效防止通过反序列化手段创建单例之外的对象.
懒汉式
针对饿汉式需要在类初始化时就创建对象, 出现了懒汉式的加载方法, 在需要时才进行单例对象的初始化. 主要有以下几种:
- 同步锁实现
- 双重锁检测实现 DoubleCheckLock
- 静态内部类单例实现
同步锁实现
|
|
只有在第一次调用 SingletonLazy.getInstance()
时才创建单例对象, 之后再调用都不会创建新的.
这种懒汉式模式能正常的运行, 也符合了单例的条件, 全局唯一, 多线程安全.
但是存在性能上的问题, 因为直接对 getInstance
方法加锁, 会导致即使单例对象已经创建了, 每次获取单例对象都会进行同步锁获取和释放, 造成资源浪费.
双重锁检测 DoubleCheckLock
为了解决上面同步锁造成的性能问题, 于是出现了 DoubleCheckLock
(DSL)机制, 只在对象没有被创建的情况下,对创建过程加同步锁:
|
|
这里对单例对象进行了两次判空:
- 第一层判断是为了避免不必要的同步, 只在对象没有创建时才走到下面的同步代码块
- 第二层判断是为了只在 null 的情况下才创建对象. 在多线程情况下,如果两个以上线程都已经运行至同步锁处,也就是都已经判断变量为空,如锁内不再次判断,会导致实例重复创建
静态内部类单例
DCL 单例模式, 虽然能满足要求, 但是不是最优雅的实现, 静态内部类单例则能利用 JVM 的类加载机制做到单例实现.
|
|
为什么静态内部类能实现线程安全的单例模式呢?
因为 JVM 的类加载机制规定, 只有以下 5 种情况才会进行立即对类进行初始化(加载, 验证, 准备需要在此之前完成) :
- 遇到
new
getstatic
putstatic
invokestatic
4 条字节码指令时, 如果类没有进行初始化, 则需要先触发初始化- 4 个字节码指令对应的 Java 场景是:
- 使用
new
创建对象 - 读取或设置一个类的静态字段
- 调用一个类的静态方法时
- 使用
- 4 个字节码指令对应的 Java 场景是:
- 通过反射获取类时, 如果类没有初始化, 则需要触发初始化
- 当初始化一个类, 发现其父类还未初始化时, 需要先初始化其父类
- 当虚拟机启动时, 用户需要指定一个包含了
main
方法的主类, 虚拟机会先初始化主类 - 使用 JDK 1.7 之后的动态支持时, 如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果是
REF_getStatic
REF_putStatic
REF_invokeStatic
的方法句柄, 并且这个方法句柄对应的类没有进行初始化, 则需要先进行初始化
外部类 SingletonByInner
的字节码:
{
private io.github.stefanji.singleton.SingletonByInner();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #2 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
public io.github.stefanji.singleton.SingletonByInner getInstance();
descriptor: ()Lio/github/stefanji/singleton/SingletonByInner;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: invokestatic #3 // Method io/github/stefanji/singleton/SingletonByInner$SingletonHolder.access$100:()Lio/github/stefanji/singleton/SingletonByInner;
3: areturn
LineNumberTable:
line 11: 0
可以看到第 18 行执行了 invokestatic
指令, 是当调用 getInstance
方法时才会走到这里. 也就说明只有当调用 getInstance
时, 才会触发内部类的初始化操作, 而且 JVM 只会初始化一个类一次, 所以就保证了内部类的静态实例在 JVM 中只有一个.
使用容器实现单例模式
使用容器实现单例模式, 其实不是单例模式的具体实现, 而是一种组织多个单例对象的方式. 每个单例对象具体的实现方式可以是上面几种.
容器对应 Java 中的 Map
List
等结构, 有时需要在全局维护多个单例时, 使用容器能方便管理.
比如 Android 中经常使用 Context
获取系统的一些服务, 其实这些服务在 Context
中都是以单例的方式存在的:
Context.getSystemService
方法:
|
|
在加载 SystemServiceRegistry
时会将常用服务创建, 并储存到单例容器 SYSTEM_SERVICE_FETCHERS
中:
|
|