IoDH实现的单例模式

在别人的代码里,看到用了一种很奇葩的方式,实现的单例模式,后来搜索了下这样实现的原因,才知道这是一个叫Initialization Demand Holder (IoDH)的技术,转了两篇,和大家分享下。

原文地址1

原文地址2

3.4 饿汉式单例与懒汉式单例的讨论

      Sunny公 司开发人员使用单例模式实现了负载均衡器的设计,但是在实际使用中出现了一个非常严重的问题,当负载均衡器在启动过程中用户再次启动该负载均衡器时,系统 无任何异常,但当客户端提交请求时出现请求分发失败,通过仔细分析发现原来系统中还是存在多个负载均衡器对象,导致分发时目标服务器不一致,从而产生冲 突。为什么会这样呢?Sunny公司开发人员百思不得其解。

      现在我们对负载均衡器的实现代码进行再次分析,当第一次调用getLoadBalancer()方法创建并启动负载均衡器时,instance对象为null值,因此系统将执行代码instance= new LoadBalancer(),在此过程中,由于要对LoadBalancer进行大量初始化工作,需要一段时间来创建LoadBalancer对象。而在此时,如果再一次调用getLoadBalancer()方法(通常发生在多线程环境中),由于instance尚未创建成功,仍为null值,判断条件(instance== null)为真值,因此代码instance= new LoadBalancer()将再次执行,导致最终创建了多个instance对象,这违背了单例模式的初衷,也导致系统运行发生错误。

      如何解决该问题?我们至少有两种解决方案,在正式介绍这两种解决方案之前,先介绍一下单例类的两种不同实现方式,饿汉式单例类和懒汉式单例类。

  

1.饿汉式单例类

      饿汉式单例类是实现起来最简单的单例类,饿汉式单例类结构图如图3-4所示:

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

 

[java] view plain copy
  1. class EagerSingleton {   
  2.     private static final EagerSingleton instance = new EagerSingleton();   
  3.     private EagerSingleton() { }   
  4.   
  5.     public static EagerSingleton getInstance() {  
  6.         return instance;   
  7.     }     
  8. }  
      当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。如果使用饿汉式单例来实现负载均衡器LoadBalancer类的设计,则不会出现创建多个单例对象的情况,可确保单例对象的唯一性。
  

2.懒汉式单例类与线程锁定

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

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

 

[java] view plain copy
  1. class LazySingleton {   
  2.     private static LazySingleton instance = null;   
  3.   
  4.     private LazySingleton() { }   
  5.   
  6.     synchronized public static LazySingleton getInstance() {   
  7.         if (instance == null) {  
  8.             instance = new LazySingleton();   
  9.         }  
  10.         return instance;   
  11.     }  
  12. }  
       该懒汉式单例类在getInstance()方法前面增加了关键字synchronized进行线程锁,以处理多个线程同时访问的问题。但是,上述代码虽然解决了线程安全问题,但是每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。如何既解决线程安全问题又不影响系统性能呢?我们继续对懒汉式单例进行改进。事实上,我们无须对整个getInstance()方法进行锁定,只需对其中的代码“instance = new LazySingleton();”进行锁定即可。因此getInstance()方法可以进行如下改进:
 
[java] view plain copy
  1. public static LazySingleton getInstance() {   
  2.     if (instance == null) {  
  3.         synchronized (LazySingleton.class) {  
  4.             instance = new LazySingleton();   
  5.         }  
  6.     }  
  7.     return instance;   
  8. }  
       问题貌似得以解决,事实并非如此。如果使用以上代码来实现单例,还是会存在单例对象不唯一。原因如下:

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

 

[java] view plain copy
  1. class LazySingleton {   
  2.     private volatile static LazySingleton instance = null;   
  3.   
  4.     private LazySingleton() { }   
  5.   
  6.     public static LazySingleton getInstance() {   
  7.         //第一重判断  
  8.         if (instance == null) {  
  9.             //锁定代码块  
  10.             synchronized (LazySingleton.class) {  
  11.                 //第二重判断  
  12.                 if (instance == null) {  
  13.                     instance = new LazySingleton(); //创建单例实例  
  14.                 }  
  15.             }  
  16.         }  
  17.         return instance;   
  18.     }  
  19. }  

       需要注意的是,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理,且该代码只能在JDK 1.5及以上版本中才能正确执行。由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。 


 

扩展

IBM公司高级软件工程师Peter    Haggar 2004年在IBM developerWorks上发表了一篇名为《双重检查锁定及单例模式——全面理解这一失效的编程习语》的文章,对JDK    1.5之前的双重检查锁定及单例模式进行了全面分析和阐述,参考链接:http://www.ibm.com/developerworks/cn/java/j-dcl.html


  

3.饿汉式单例类与懒汉式单例类比较

      饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开 始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉 式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。

      懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例 化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,需要通过双重检查锁定等机制进行控制, 这将导致系统性能受到一定影响。


————————————————


3.5 一种更好的单例实现方法

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

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

 

[java] view plain copy
  1. //Initialization on Demand Holder  
  2. class Singleton {  
  3.     private Singleton() {  
  4.     }  
  5.       
  6.     private static class HolderClass {  
  7.             private final static Singleton instance = new Singleton();  
  8.     }  
  9.       
  10.     public static Singleton getInstance() {  
  11.         return HolderClass.instance;  
  12.     }  
  13.       
  14.     public static void main(String args[]) {  
  15.         Singleton s1, s2;   
  16.             s1 = Singleton.getInstance();  
  17.         s2 = Singleton.getInstance();  
  18.         System.out.println(s1==s2);  
  19.     }  
  20. }  

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

      通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH)。


 

练习

分别使用饿汉式单例、带双重检查锁定机制的懒汉式单例以及IoDH技术实现负载均衡器LoadBalancer


      至此,三种单例类的实现方式我们均已学习完毕,它们分别是饿汉式单例、懒汉式单例以及IoDH


你可能感兴趣的:(设计模式)