单例设计模式是 GoF23 种设计模式中最常用的设计模式之一,无论是第三方类库,还是我们在日常的开发中,几乎都可以看到单例的影子,单例设计模式提供了一种在多线程情况下保证实例唯一性的解决方案,单例设计模式的实现虽然非常简单,但是实现方式却多种多样。我们从三个维度来对下面7种单列设计模式进行评估:线程安全、高性能、懒加载。
1、饿汉式
public final class Singleton1 {
// 实例变量
private byte[] data = new byte[1024];
// 在定义实例对象的时候直接初始化
private static Singleton1 instance = new Singleton1();
// 私有构造函数,不允许外部new
private Singleton1() {
}
public static Singleton1 getInstance() {
return instance;
}
}
饿汉式的关键在于 instance 作为类变量并且直接得到了初始化,如果主动使用 Singleton类,那么instance实例将会直接完成创建,包括其中的实例变量都会得到初始化,比如 1K 空间的data将会同时被创建。
instance 作为类变量在类初始化的过程中会被收集进 <clinit>() 方法中,该方法能够百分之百地保证同步,也就是说 instance 在多线程的情况下不可能被实例化两次,但是 instance被 ClassLoader 加载后可能很长一段时间才被使用,那就意味着 instance 实例所开辟的堆内存会驻留更久的时间。
如果一个类中的成员属性比较少,且占用的内存资源不多,饿汉的方式也未尝不可,相反,如果一个类中的成员都是比较重的资源,那么这种方式就会有些不妥。
总结起来,饿汉式的单例设计模式可以保证多个线程下的唯一实例,getInstance 方法性能也比较高,但是无法进行懒加载。
2、懒汉式
所谓懒汉式就是在使用类实例的时候再去创建(用时创建),这样就可以避免类在初始化时提前创建。
public final class Singleton2 {
// 实例变量
private byte[] data = new byte[1024];
// 在定义实例对象的时候直接初始化
private static Singleton2 instance = null;
// 私有构造函数,不允许外部new
private Singleton2() {
}
public static Singleton2 getInstance() {
if(null == instance){
instance = new Singleton2();
}
return instance;
}
}
Singleton 的类变量 instance = null,因此当 Singleton.class 被初始化的时候instance 并不会被实例化,在 getInstance 方法中会判断 instance 实例是否被实例化,看起来没有什么问题,但是将 getInstance 方法放在多线程环境下进行分析,则会导致 instance被实例化一次以上,并不能保证单例的唯一性,如图所示。
两个线程同时看到 instance==null,那么instance将无法保证单例的唯一性。
3、懒汉式+同步方法
懒汉式的方式可以保证实例的懒加载,但无法保证实例的唯一性。在多线程的情况下,instance 又称为共享资源(数据),当多个线程对其访问使用时,需要保证数据的同步性,对 2 中的程序稍加修改,增加同步的约束即可,修改后的代码如下所示。
public final class Singleton3 {
// 实例变量
private byte[] data = new byte[1024];
// 在定义实例对象的时候直接初始化
private static Singleton3 instance = null;
// 私有构造函数,不允许外部new
private Singleton3() {
}
// 向getInstance方法加入同步控制,每次只能有一个线程能够进入
public static synchronized Singleton3 getInstance() {
if(null == instance){
instance = new Singleton3();
}
return instance;
}
}
采用懒汉式+数据同步的方式既满足了懒加载又能够百分之百地保证 instance 实例的唯一性,但是 synchronized 关键字天生的排他性导致了 getInstance 方法只能在同一时刻被一个线程所访问,性能低下。
4、Double-Check
Double-Check是一种比较聪明的设计方式,他提供了一种高效的数据同步策略,那就是首次初始化时加锁,之后则允许多个线程同时进行 getInstance 方法的调用来获得类的实例。
public final class Singleton4 {
// 实例变量
private byte[] data = new byte[1024];
// 定义实例,但是不直接初始化
private static Singleton4 instance = null;
private Object o1;
private Object o2;
// 私有构造函数,不允许外部new
private Singleton4() {
this.o1 = new Object(); // 初始化o1
this.o2 = new Object(); // 初始化o1
}
// 向getInstance方法加入同步控制,每次只能有一个线程能够进入
public static synchronized Singleton4 getInstance() {
// 当instance为 null 时,进入同步代码块,同时该判断避免了每次都需要进入同步代码块,可以提高效率
if(null == instance){
// 只有一个线程能够获得 Singleton.class 关联的monitor
synchronized (Singleton4.class){
// 判断如果 instance 为 null 则创建
if(null == instance){
instance = new Singleton4();
}
}
}
return instance;
}
}
当两个线程发现 null==instance 成立时,只有一个线程有资格进人同步代码块,完成对instance 的实例化,随后的线程发现 null==instance 不成立则无须进行任何动作,以后对getInstance 的访问就不需要数据同步的保护了。
这种方式看起来是那么的完美和巧妙,既满足了懒加载,又保证了 instance 实例的唯一性,Double-Check 的方式提供了高效的数据同步策略,可以允许多个线程同时对 getInstance 进行访问,但是这种方式在多线程的情况下有可能会引起空指针异常,下面我们来分析一下引发异常的原因。
在 Singleton 的构造函数中,需要分别实例化 o1 和 o2 两个资源,还有 Singleton 自身,根据 JVM 运行时指令重排序和 Happens-Before 规则,这三者之间的实例化顺序并无前后关系的约束,那么极有可能是 instance 最先被实例化,而 o1 和 o2 并未完成实例化,未完成初始化的实例调用其方法将会抛出空指针异常。
5、Volatile+Double-Check
Double-Check虽然是一种巧妙的程序设计,但是有可能会引起类成员变量的实例化 o1 和 o2 发生在 instance 实例化之后,这一切均是由于 JVM 在运行时指令重排序所导致的,而volatile 关键字则可以防止这种重排序的发生,因此代码稍作修改即可满足多线程下的单例、懒加载以及获取实例的高效性,代码修改如下:
private volatile static singleton instance = null;
6、Holder方式
Holder 的方式完全是借助了类加载的特点,下面我们对整个单例模式进行重构,然后结合类加载器的知识点分析这样做的好处在哪里。
public final class Singleton6 {
// 实例变量
private byte[] data = new byte[1024];
// 私有构造函数,不允许外部new
private Singleton6() {
}
// 在静态内部类中持有singleton的实例,并且可被直接初始化
private static class Holder{
private static Singleton6 instance = new Singleton6();
}
// 调用getInstance方法,事实上是获得 Holder 的 instance 静态属性
public static synchronized Singleton6 getInstance() {
return Holder.instance;
}
}
在 Singleton 类中并没有 instance 的静态成员,而是将其放到了静态内部类 Holder 之中,因此在 Singleton 类的初始化过程中并不会创建 Singleton 的实例,Holder类中定义了Singleton 的静态变量,并且直接进行了实例化,当 Holder 被主动引用的时候则会创建Singleton 的实例,Singleton 实例的创建过程在 Java 程序编译时期收集至 <clinit>() 方法中,该方法又是同步方法,同步方法可以保证内存的可见性、JVM指令的顺序性和原子性。Holder方式的单例设计是最好的设计之一,也是目前使用比较广的设计之一。
7、枚举方式
使用枚举的方式实现单例模式是《Effective Java》作者力推的方式,在很多优秀的开源代码中经常可以看到使用枚举方式实现单例模式的(身影),枚举类型不允许被继承,同样是线程安全的且只能被实例化一次,但是枚举类型不能够懒加载,对 Singleton 主动使用,比如调用其中的静态方法则 INSTANCE 会立即得到实例化。
// 枚举类型本身是 final 的,不允许被继承
public enum Singleton7 {
INSTANCE;
// 实例变量
private byte[] data = new byte[1024];
// 私有构造函数,不允许外部new
private Singleton7() {
}
// 调用getInstance方法,事实上是获得 Holder 的 instance 静态属性
public static Singleton7 getInstance() {
return INSTANCE;
}
}
但是也可以对其进行改造,增加懒加载的特性,类似于 Holder 的方式,改进后的代码如下所示。
// 枚举类型本身是 final 的,不允许被继承
public class Singleton8 {
// 实例变量
private byte[] data = new byte[1024];
// 私有构造函数,不允许外部new
private Singleton8() {
}
private enum EnumHolder {
INSTANCE;
private Singleton8 instance;
EnumHolder() {
this.instance = new Singleton8();
}
private Singleton8 getSingleton8() {
return instance;
}
}
public static Singleton8 getInstance() {
return EnumHolder.INSTANCE.getSingleton8();
}
}