前言
单例模式是设计模式中非常基础的一种模式,有多种写法。本文主要分析常见的几种写法的优缺点进行简单的分析和说明。
单例模式,顾名思义就是这个类只有一个实例,并且只具有私有的构造器,外部无法通过类的构造器来创建实例。
常见的五种写法
1. 饿汉式
public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() { } public static Singleton getInstance() { return INSTANCE; }}复制代码
饿汉式的写法,getInstance()
方法不需要加同步关键字 synchronized
,因为实例已经在类加载的时候已经创建好了,所有的线程调用 getInstance()
方法,都拿到的是同一个实例。
2. 懒汉式
public class Singleton { private static Singleton INSTANCE; private Singleton() { } public synchronized static Singleton getInstance() { if (INSTANCE == null) { INSTANCE = new Singleton(); } return INSTANCE; }}复制代码
懒汉式的写法,getInstance
方法需要加同步关键字 synchronized
,确保同一时刻只有一个线程进入 getInstance
方法代码块。
懒汉式的写法相当于延迟创建实例。有一个缺点,客户端每次调用 getInstance
方法时都做了同步,即便 INSTANCE
已经不为 null
了,导致会有性能损耗。
3. 双重检查加锁
public class Singleton { private volatile static Singleton INSTANCE; //volatile 关键字 private Singleton() { } public static Singleton getInstance() { if (INSTANCE == null) { //第一次检查 synchronized (Singleton.class) { if (INSTANCE == null) { //第二次检查 INSTANCE = new Singleton(); } } } return INSTANCE; }}复制代码
双重检查加锁的版本,可以理解为懒汉式写法上的一种改进。解决了懒汉式每次调用getInstance
方法都需要同步的缺点,性能上会比懒汉式有更好的表现。
有3个地方需要注意:
- 第一次检查,当实例被创建后,即
INSTANCE != null
时,调用getInstance
会直接返回单实例,没有做同步,这就是相比懒汉式写法优化的点。 - 第二次检查是必须的,假如没有第二次检查,两个线程 A, B 同时通过第一次检查后,到达同步块外边,线程 A 拿到同步锁创建实例,释放锁;接着,线程 B 拿到同步锁,再次创建一个实例。就创建了不止一个实例,违背了单例的原则。
volatile
关键字修饰INSTANCE
静态域。加了这个关键字后,能够保证调用getInstance
方法时,各个线程里面INSTANCE
的状态保持同步。例如线程 A 设置了INSTANCE
的值,那线程 B 马上就会知道,避免了状态不同步导致创建多个实例的可能。
4. 静态内部类
public class Singleton { private Singleton() { } public static Singleton getInstance() { return Holder.INSTANCE; } private static class Holder { private static final Singleton INSTANCE = new Singleton(); }}复制代码
第一次调用getInstance
方法时,Holder.INSTANCE
第一次被读取,静态内部类Holder
得到初始化,Holder
下的静态域被初始化。只会在 JVM 装载Holder
类的时候初始化一次,并由 JVM 来保证它的线程安全。
这里线程安全是通过 JVM 的类装载机制保证的,所以在 getInstance
方法定义时不需要添加synchronized
关键字。
5. 枚举
public enum Singleton { INSTANCE; public void method() {}}复制代码
枚举单例实现相比双重检查加锁和静态内部类的实现,要简洁很多。双重检查加锁需要保证线程安全,所以很多代码都是在处理同步问题。
静态内部类的实现,新增了一个静态内部类,利用 JVM 的类装载机制来保证线程安全,代码量上也会有所增加。
而枚举单例的线程安全不需要我们关心,根本上也是利用了 JVM 的类装载来保证线程安全。
枚举实现原理
通过 javac 编译 Singleton.java 文件,生成了一个 Singleton.class 文件,再通过 jad 反编译工具对 Singleton.class 文件进行反编译,反编译后的代码如下:
public final class Singleton extends Enum{ public static Singleton[] values() { return (Singleton[])$VALUES.clone(); } public static Singleton valueOf(String s) { return (Singleton)Enum.valueOf(test/Singleton, s); } private Singleton(String s, int i) { super(s, i); } public void method() { } public static final Singleton INSTANCE; private static final Singleton $VALUES[]; static { INSTANCE = new Singleton("INSTANCE", 0); $VALUES = (new Singleton[] { INSTANCE }); }}复制代码
通过上面的反编译代码可知,枚举类型编译后,会生成一个继承自 Enum 的类,并且枚举的单实例被编译成了一个静态域,在这个类的 static 块里面初始化这个静态域。
static 块会在类第一次被加载的时候执行,而 JVM 装载类是线程安全的,所以枚举单例根本上还是利用了 JVM 的类装载机制来保证线程安全,跟静态内部类的机制有点相似。但枚举写法的代码量上要少很多。
总结
单例模式有多种写法,围绕的核心问题就是怎样解决线程安全和性能问题,保证单例的特性,并且在性能有良好的表现。
-
饿汉式写法实例初始化太早,在不需要的时候也会进行实例化,可能导致资源消耗;
-
懒汉式写法延迟加载了实例,但每次获取实例时,都要进行同步,性能上会有消耗;
-
双重检查加锁写法是对懒汉式的改良,只有通过第一重检查后,才会进行同步。如果实例已经被创建,后面再调用获取实例方法,不会再进入同步块。代码量相对比较多。
-
静态内部类写法是利用了 JVM 装载类机制的线程安全特性,所以我们不需要处理同步相关的逻辑,JVM 已经帮我们处理好了。代码量相对比较多。
-
枚举单例底层也是利用了 JVM 装载类是线程安全特性,代码量非常少。另外,对于类实现
Serializable
情况,枚举的反序列化不是通过反射实现,所以也就不会发生由反序列化导致的单例破坏问题。
对于实现序列化和反射破坏单例的情况,本文不涉及,感兴趣的同学可以找找相关资料。