在用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