基于IoDH技术实现的单例模式

IoDH全称Initialization Demand Holder其技术完美解决了懒汉模式的多线程问题, 同时也解决了饿汉模式的性能低下的问题

一、前言

首先介绍目前常见的两种设计模式,懒汉式和饿汉式。但是这两种设计模式都多多少少有一些问题。

一、饿汉式

从图3-4中可以看出,由于在定义静态变量的时候实例化单例类,因此在类加载的时候就已经创建了单例对象,代码如下所示:

1
2
3
4
5
6
7
8
class EagerSingleton { 
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() { }

public static EagerSingleton getInstance() {
return instance;
}
}

当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。如果使用饿汉式单例来实现负载均衡器LoadBalancer类的设计,则不会出现创建多个单例对象的情况,可确保单例对象的唯一性。

二、懒汉式

 除了饿汉式单例,还有一种经典的懒汉式单例,也就是前面的负载均衡器LoadBalancer类的实现方式。懒汉式单例类结构图如图3-5所示

从图3-5中可以看出,懒汉式单例在第一次调用getInstance()方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载(Lazy Load)技术,即需要的时候再加载实例,为了避免多个线程同时调用getInstance()方法,我们可以使用关键字synchronized,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
class LazySingleton { 
private static LazySingleton instance = null;

private LazySingleton() { }

synchronized public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}

但是这种方法在并发状况下,会因为锁的存在大幅降低性能,并且依旧可能出现单例不唯一的状况

假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过instance == null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在synchronized中再进行一次(instance == null)判断,这种方式称为双重检查锁定(Double-Check Locking)。使用双重检查锁定实现的懒汉式单例类完整代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class LazySingleton { 
private volatile static LazySingleton instance = null;

private LazySingleton() { }

public static LazySingleton getInstance() {
//第一重判断
if (instance == null) {
//锁定代码块
synchronized (LazySingleton.class) {
//第二重判断
if (instance == null) {
instance = new LazySingleton(); //创建单例实例
}
}
}
return instance;
}
}

二、IoDH单例模式

饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。可见,无论是饿汉式单例还是懒汉式单例都存在这样那样的问题,有没有一种方法,能够将两种单例的缺点都克服,而将两者的优点合二为一呢?答案是:Yes!下面我们来学习这种更好的被称之为**Initialization Demand Holder (IoDH)**的技术。

      在IoDH中,我们在单例类中增加一个静态(static)内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用,实现代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Initialization on Demand Holder
class Singleton {
private Singleton() {
}

private static class HolderClass {
private final static Singleton instance = new Singleton();
}

public static Singleton getInstance() {
return HolderClass.instance;
}

public static void main(String args[]) {
Singleton s1, s2;
s1 = Singleton.getInstance();
s2 = Singleton.getInstance();
System.out.println(s1==s2);
}
}

编译并运行上述代码,运行结果为:true,即创建的单例对象s1和s2为同一对象。由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。

通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式

三、弊端

SingleTon类被JVM加载时,由于这个类没有其他静态属性,其初始化过程会顺利完成。但是内部静态类Holder直到调用getInstance()时才会被初始化。 当Holder第一次被执行时,JVM会加载并初始化该类。由于Holder含有静态方法INSTANCE,因此会一并初始化INSTANCE。JLS保证了类的初始化阶段是连续的。这样,所有后序的并发调用getInstance()都会返回一个正确初始化的INSTANCE而不会有额外同步开销。 但是,任何初始化失败都会导致单例类不可用。也就是说,IoDH这种实现方式只能用于能保证初始化不会失败的情况。

四、引用目录