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已經比較快了) 三、若是須要懶加載,且不怕麻煩,可使用方法二。 四、若是須要懶加載,沒有且!推薦使用方法四。