基于IoDH技术实现的单例模式
IoDH全称Initialization Demand Holder其技术完美解决了懒汉模式的多线程问题, 同时也解决了饿汉模式的性能低下的问题
一、前言
首先介绍目前常见的两种设计模式,懒汉式和饿汉式。但是这两种设计模式都多多少少有一些问题。
一、饿汉式
从图3-4中可以看出,由于在定义静态变量的时候实例化单例类,因此在类加载的时候就已经创建了单例对象,代码如下所示:
1 | class EagerSingleton { |
当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。如果使用饿汉式单例来实现负载均衡器LoadBalancer类的设计,则不会出现创建多个单例对象的情况,可确保单例对象的唯一性。
二、懒汉式
除了饿汉式单例,还有一种经典的懒汉式单例,也就是前面的负载均衡器LoadBalancer类的实现方式。懒汉式单例类结构图如图3-5所示
从图3-5中可以看出,懒汉式单例在第一次调用getInstance()方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载(Lazy Load)技术,即需要的时候再加载实例,为了避免多个线程同时调用getInstance()方法,我们可以使用关键字synchronized,代码如下所示:
1 | class LazySingleton { |
但是这种方法在并发状况下,会因为锁的存在大幅降低性能,并且依旧可能出现单例不唯一的状况
假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过instance == null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在synchronized中再进行一次(instance == null)判断,这种方式称为双重检查锁定(Double-Check Locking)。使用双重检查锁定实现的懒汉式单例类完整代码如下所示:
1 | class LazySingleton { |
二、IoDH单例模式
饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。可见,无论是饿汉式单例还是懒汉式单例都存在这样那样的问题,有没有一种方法,能够将两种单例的缺点都克服,而将两者的优点合二为一呢?答案是:Yes!下面我们来学习这种更好的被称之为**Initialization Demand Holder (IoDH)**的技术。
在IoDH中,我们在单例类中增加一个静态(static)内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用,实现代码如下所示:
1 | //Initialization on Demand Holder |
编译并运行上述代码,运行结果为: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这种实现方式只能用于能保证初始化不会失败的情况。