静态工厂方法中单例的延迟加载技术

  在用Java实际开发的时候我们经常要用到单例模式,一般来说,单例类的实例只能通过静态工厂方法来创建,我们也许会这样写:

public final class Singleton  
{  
    private static Singleton singObj = new Singleton();  
  
    private Singleton(){  
    }  
  
    public static Singleton getSingleInstance(){  
       return singObj;
    }  
}  

  这种方法可能会带来潜在的性能问题:如果这个对象很大呢?没有使用这个对象之前,就把它加载到了内存中去是一种巨大的浪费。因此我们希望在用到它的时候再加载它,这种设计思想就是延迟加载(Lazy-load Singleton)。

public final class LazySingleton  
{  
    private static LazySingleton singObj = null;  
  
    private LazySingleton(){  
    }  
  
    public static LazySingleton getSingleInstance(){  
        if(null == singObj )    //A线程执行
       singObj = new LazySingleton(); //B线程执行 return singObj; } }

  上面的写法就保证了在对象使用之前是不会被初始化的。这种方式对于单线程来说是没什么问题,但是在多线程的环境下却是不安全的。假设A线程执行代码1的同时,B线程执行代码2。此时,线程A可能会看到instance引用的对象还没有完成初始化。如何避免这个问题,答案很简单,在那个方法前面加一个Synchronized就OK了。

public final class ThreadSafeSingleton  
{  
    private static ThreadSafeSingleton singObj = null;  
  
    private ThreadSafeSingleton(){  
    }  
  
    public static Synchronized ThreadSafeSingleton getSingleInstance(){  
        if(null == singObj ) singObj = new ThreadSafeSingleton();
            return singObj;
    }  
}  

  但是众所周知,同步对性能是有影响的。由于对getInstance()做了同步处理,synchronized将导致性能开销。如果getInstance()被多个线程频繁的调用,将会导致程序执行性能的下降。那么有没有什么方法,一方面是线程安全的,有可以有很高的并发度呢?我们的能想到的对策是降低同步的粒度,因此,人们想出了一个“聪明”的技巧:双重检查锁定(double-checked locking)。

public final class DoubleCheckedSingleton  
{  
    private static DoubleCheckedSingletonsingObj = null;  
  
    private DoubleCheckedSingleton(){  
    }  
  
    public static DoubleCheckedSingleton getSingleInstance(){  
        if(null == singObj ) {                                //第一次检查
              Synchronized(DoubleCheckedSingleton.class){     //加锁
                     if(null == singObj)                      //第二次检查
                           singObj = new DoubleCheckedSingleton();
              }
         }
       return singObj;
    }  
}  

  如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。这看起来似乎两全其美,但是这是一个错误的优化!原因在于singObj = new DoubleCheckedSingleton()这一句。

  以上有问题的这一行代码可以分解为如下的三行伪代码:

memory = allocate();   //1:分配对象的内存空间
ctorInstance(memory);  //2:初始化对象
instance = memory;     //3:设置instance指向刚分配的内存地址

  在一些编译器中会对代码进行优化而对语句进行重排序,这很常见,对于上面的三行伪代码,2和3可能会被重排,结果如下:

memory = allocate();   //1:分配对象的内存空间
instance = memory;     //3:设置instance指向刚分配的内存地址
                       //注意,此时对象还没有被初始化!
ctorInstance(memory);  //2:初始化对象

  所以在多线程的环境下线程A可能执行到3,这时候对象在内存中已经有地址了,但是还未被完全初始化,这时候一旦线程B执行,在第一次检查的时候(null == singObj)返回false,线程B以为对象已经存在,接下来就可以使用了。实际上线程B可能使用了一个没有被完全初始化的对象,运行结果不得而知。

   对于这种问题解决方法有两种,“基于volatile的双重检查锁定的解决方案”和“基于类初始化的解决方案”,在双重检查锁定与延迟初始化这篇文章中有详细介绍。我们来说下第二种解决方案。这种解决方案也就是Initialization On Demand Holder idiom,这种方法使用内部类来做到延迟加载对象,在初始化这个内部类的时候,JLS(Java Language Sepcification)会保证这个类的线程安全。这种写法最大的美在于,完全使用了Java虚拟机的机制进行同步保证,没有一个同步的关键字。

public class Singleton    
{    
    private static class SingletonHolder    
    {    
        public final static Singleton instance = new Singleton();    
    }    
   
    public static Singleton getInstance()    
    {    
        return SingletonHolder.instance;    
    }    
}

  内部类的初始化是延迟的,外部类初始化时不会初始化内部类,只有在使用的时候才会初始化内部类。而Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应。也就是说,SingletonHolder在各个线程初始化的时候是同步执行的,且全权由JVM承包了。

  Initialization On Demand Holder idiom的实现探讨中分析了单例的几种描述符(private static final / public static final / static final)之间的合理性,其推荐使用最后一种描述符方式更为合理。

 

 

参考资料:

http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization

http://ifeve.com/initialization-on-demand-holder-idiom/ 

http://stackoverflow.com/questions/20995036/is-initialization-on-demand-holder-idiom-thread-safe-without-a-final-modifier

http://stackoverflow.com/questions/21604243/correct-implementation-of-initialization-on-demand-holder-idiom

http://blog.sina.com.cn/s/blog_75247c770100yxpb.html

 

posted @ 2014-11-04 20:14  Mr.Open  阅读(1654)  评论(0编辑  收藏  举报