史上最清晰最透徹——Java單例模式分析

lazy模式:html

其優缺點已經在備註中註明,來重點分析一下非線程安全的部分:java

一、當線程A進入到第28行(#1)時,檢查instance是否爲空,此時是空的。
二、此時,線程B也進入到28行(#1)。切換到線程B執行。一樣檢查instance爲空,因而往下執行29行(#2),建立了一個實例。接着返回了。
三、在切換回線程A,因爲以前檢查到instance爲空。因此也會執行29行(#2)建立實例。返回。
四、至此,已經有兩個實例被建立了,這不是咱們所但願的。 安全

那麼怎麼去解決線程安全問題jvm

第一種方法,在上圖中的getInstance()方法加上synchronized關鍵字ide

加上synchronized後確實實現了線程的互斥訪問getInstance()方法。從而保證了線程安全。可是這樣就完美了麼?咱們看。其實在第一種lazy模式實現裏,會致使問題的只是當instance尚未被實例化的時候,多個線程訪問#1的代碼纔會致使問題。而當instance已經實例化完成後。每次調用getInstance(),其實都是直接返回的。即便是多個線程訪問,也不會出問題。但給方法加上synchronized後。全部getInstance()的調用都要同步了。其實咱們只是在第一次調用的時候要同步。而同步須要消耗性能。這就是問題性能

方法二:雙重檢查加鎖Double-checked locking。
  
其實通過分析發現,咱們只要保證 instance = new SingletonOne(); 是線程互斥訪問的就能夠保證線程安全了。那把同步方法加以改造,只用synchronized塊包裹這一句。就獲得了下面的代碼:spa

 

這個方法可行麼?分析一下發現是不行的!
  一、線程A和線程B同時進入//1的位置。這時instance是爲空的。
  二、線程A進入synchronized塊,建立實例,線程B等待。
  三、線程A返回,線程B繼續進入synchronized塊,建立實例。。。
  四、這時已經有兩個實例建立了。 線程

  爲了解決這個問題。咱們須要在//2的以前,再加上一次檢查instance是否被實例化。(雙重檢查加鎖)接下來,代碼變成了這樣:code

 

這樣,當線程A返回,線程B進入synchronized塊後,會先檢查一下instance實例是否被建立,這時實例已經被線程A建立過了。因此線程B不會再建立實例,而是直接返回。貌似!到此爲止,這個問題已經被咱們完美的解決了。遺憾的是,事實徹底不是這樣!這個方法在單核和 多核的cpu下都不能保證很好的工做。致使這個方法失敗的緣由是當前java平臺的內存模型。java平臺內存模型中有一個叫「無序寫」(out-of-order writes)的機制。正是這個機制致使了雙重檢查加鎖方法的失效。這個問題的關鍵在上面代碼上的第5行:instance = new SingletonThree(); 這行其實作了兩個事情:一、調用構造方法,建立了一個實例。二、把這個實例賦值給instance這個實例變量。可問題就是,這兩步jvm是不保證順序的。也就是說。可能在調用構造方法以前,instance已經被設置爲非空了。下面咱們看一下出問題的過程:
  一、線程A進入getInstance()方法。
  二、由於此時instance爲空,因此線程A進入synchronized塊。
  三、線程A執行 instance = new SingletonThree(); 把實例變量instance設置成了非空。(注意,實在調用構造方法以前。)
  四、線程A退出,線程B進入。
  五、線程B檢查instance是否爲空,此時不爲空(第三步的時候被線程A設置成了非空)。線程B返回instance的引用。(問題出現了,這時instance的引用並非SingletonThree的實例,由於沒有調用構造方法。) 
  六、線程B退出,線程A進入。
  七、線程A繼續調用構造方法,完成instance的初始化,再返回。 orm

  好吧,繼續努力,解決由「無序寫」帶來的問題。

 

解釋一下執行步驟。
  一、線程A進入getInstance()方法。
  二、由於instance是空的 ,因此線程A進入位置//1的第一個synchronized塊。
  三、線程A執行位置//2的代碼,把instance賦值給本地變量temp。instance爲空,因此temp也爲空。 
  四、由於temp爲空,因此線程A進入位置//3的第二個synchronized塊。
  五、線程A執行位置//4的代碼,把temp設置成非空,但尚未調用構造方法!(「無序寫」問題) 
  六、線程A阻塞,線程B進入getInstance()方法。
  七、由於instance爲空,因此線程B試圖進入第一個synchronized塊。但因爲線程A已經在裏面了。因此                              沒法進入。線程B阻塞。
  八、線程A激活,繼續執行位置//4的代碼。調用構造方法。生成實例。
  九、將temp的實例引用賦值給instance。退出兩個synchronized塊。返回實例。
  十、線程B激活,進入第一個synchronized塊。
  十一、線程B執行位置//2的代碼,把instance實例賦值給temp本地變量。
  十二、線程B判斷本地變量temp不爲空,因此跳過if塊。返回instance實例。

  好吧,問題終於解決了,線程安全了。可是咱們的代碼由最初的3行代碼變成了如今的一大坨~。因而又有了下面的方法。

預先初始化static變量

/**
 * 預先初始化static變量 的單例模式  非Lazy  線程安全
 * 優勢:
 * 一、線程安全 
 * 缺點:
 * 一、非懶加載,若是構造的單例很大,構造完又遲遲不使用,會致使資源浪費。
 * 
 * @author laichendong
 * @since 2011-12-5
 */
public class SingletonFour {
    
    /** 單例變量 ,static的,在類加載時進行初始化一次,保證線程安全 */
    private static SingletonFour instance = new SingletonFour();
    
    /**
     * 私有化的構造方法,保證外部的類不能經過構造器來實例化。
     */
    private SingletonFour() {
        
    }
    
    /**
     * 獲取單例對象實例
     * 
     * @return 單例對象
     */
    public static SingletonFour getInstance() {
        return instance;
    }
    
}

看到這個方法,世界又變得清淨了。因爲java的機制,static的成員變量只在類加載的時候初始化一次,且類加載是線程安全的。因此這個方法實現的單例是線程安全的。可是這個方法卻犧牲了Lazy的特性。單例類加載的時候就實例化了。如註釋所述:非懶加載,若是構造的單例很大,構造完又遲遲不使用,會致使資源浪費。

那到底有沒有完美的辦法?懶加載,線程安全,代碼簡單。

使用內部類。

/**
 * 基於內部類的單例模式  Lazy  線程安全
 * 優勢:
 * 一、線程安全
 * 二、lazy
 * 缺點:
 * 一、待發現
 * 
 * @author laichendong
 * @since 2011-12-5
 */
public class SingletonFive {
    
    /**
     * 內部類,用於實現lzay機制
*/
    private static class SingletonHolder{
        /** 單例變量  */
        private static SingletonFive instance = new SingletonFive();
    }
    
    /**
     * 私有化的構造方法,保證外部的類不能經過構造器來實例化。
*/
    private SingletonFive() {
        
    }
    
    /**
     * 獲取單例對象實例
     * 
     * @return 單例對象
*/
    public static SingletonFive getInstance() {
        return SingletonHolder.instance;
    }
    
}

解釋一下,由於java機制規定,內部類SingletonHolder只有在getInstance()方法第一次調用的時候纔會被加載(實現了lazy),並且其加載過程是線程安全的(實現線程安全)。內部類加載的時候實例化一次instance。

  最後,總結一下:   一、若是單例對象不大,容許非懶加載,可使用方法三。   二、若是須要懶加載,且容許一部分性能損耗,可使用方法一。(官方說目前高版本的synchronized已經比較快了)   三、若是須要懶加載,且不怕麻煩,可使用方法二。   四、若是須要懶加載,沒有且!推薦使用方法四。 

相關文章
相關標籤/搜索