靜態工廠方法中單例的延遲加載技術

  在用Java實際開發的時候咱們常常要用到單例模式,通常來講,單例類的實例只能經過靜態工廠方法來建立,咱們也許會這樣寫:html

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可能會被重排,結果以下:spa

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承包了。code

  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

相關文章
相關標籤/搜索