懶漢式程序員
顧名思義,lazy loading(延遲加載,一說懶加載),在須要的時候才建立單例對象,而不是隨着軟件系統的運行或者當類被加載器加載的時候就建立。當單例類的建立或者單例對象的存在會消耗比較多的資源,經常採用lazy loading策略。這樣作的一個明顯好處是提升了軟件系統的效率,節約內存資源。下面咱們看看最簡單的懶漢單例模式:安全
代碼1-1多線程
?ide
1函數 2性能 3測試 4spa 5.net 6線程 7 8 9 10 11 12 13 14 15 16 |
|
在單線程環境下,屢次調用getInstance()方法得到的Singleton對象均爲同一個對象,單例模式實現成功。然而,在更多時候,軟件系統工做於多線程環境下,所以不得不考慮線程安全的問題。
現有多線程測試程序以下:
代碼1-2
1 2 3 4 5 6 7 8 9 10 |
|
代碼中先建立了一個實現了Runnable接口的匿名類對象run,而後用for循環建立並啓動50個線程,其中一次運行結果以下:
1 2 3 4 |
|
顯然,Singleton的構造方法不止一次被調用,也就是說,Singleton存在四個實例對象,這違背了單例模式的初衷。這個實驗說明,簡單的懶漢式在多線程環境下不是線程安全的。有人提出在getInstance()方法上同步鎖,可是鎖住一整個方法可能粒度過大,不利於效率。既然鎖方法不太好,那麼鎖代碼呢?下面咱們再看看兩個例子:
代碼1-3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
代碼段1-3的getInstance()方法裏,在判空語句後上鎖,把singleton = new Singleton()語句鎖住了,這樣作看似解決了線程安全問題,其實否則。設現有線程A和B,在t1時刻線程A和B均已經過判空語句但都未取得鎖資源;t2時刻時,A先取得鎖資源進入臨界區(被鎖的代碼塊),執行new操做建立實例對象,而後退出臨界區,釋放鎖資源。t3時刻,B取得被A釋放的鎖資源進入臨界區,執行new操做建立實例對象,而後退出臨界區,釋放鎖資源。明顯地,Singleton被實例化兩次。因此,如代碼段1-3這樣寫也不能保證線程安全。
代碼1-4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
代碼段1-4把代碼鎖放在了判空語句前,這樣作避免了代碼段1-3的問題,然而這樣作相似於在方法簽名上加上synchronized關鍵字,會影響程序效率。由於當有多個線程幾乎同時訪問getInstance方法時,多個線程必須有次序地進入方法內,這樣致使了若干個線程須要耗費等待進入臨界區(被鎖住的代碼塊)的時間。基於此,有人提出了雙重校驗鎖式。
雙重校驗鎖DCL(double checked locking)
雙重校驗鎖式(也有人把雙重校驗鎖式和懶漢式歸爲一類)分別在代碼鎖先後進行判空校驗,避免了多個有機會進入臨界區的線程都建立對象,同時也避免了代碼段1-4後來線程在先來線程建立對象後但仍未退出臨界區的狀況下等待。雙重校驗鎖代碼以下:
代碼2-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
經屢次試驗說明,雙重校驗鎖式是線程安全的。然而,在JDK1.5之前,DCL是不穩定的,有時也可能建立多個實例,在1.5之後開始提供volatile關鍵字修飾變量來達到穩定效果。
餓漢式
單例模式的餓漢式,在定義自身類型的成員變量時就將其實例化,使得在Singleton單例類被系統(姑且這麼說)加載時就已經被實例化出一個單例對象,從而一勞永逸地避免了線程安全的問題。代碼以下:
代碼3-1
1 2 3 4 5 6 7 8 9 10 11 |
|
使用多線程測試代碼1-2進行測試,單例模式成功實現。
我想應該有朋友對餓漢式單例在什麼時候被實例化感興趣。能夠編寫以下簡單測試代碼:
代碼3-2
1 2 3 4 5 |
|
運行這段代碼後能夠看到控制檯有「構造函數被調用」字符串輸出,說明在ClassLoader加載Singleton類時,餓漢式單例就被建立。
雖然餓漢式單例是線程安全的,但也有其不足之處。餓漢式單例在類被加載時就建立單例對象而且長駐內存,無論你需不須要它;若是單例類佔用的資源比較多,就會下降資源利用率以及程序的運行效率。有一種更高級的單例模式則很好地解決了這個問題——靜態內部類。
IoDH(Initialization Demand Holder)——經過靜態內部類實現線程安全的單例模式
靜態內部類式在Singleton類內部定義了一個靜態的內部類,在該內部類裏建立Singleton的單例對象。咱們先看代碼:
代碼4-1
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
靜態內部類式和餓漢式同樣,一樣利用了ClassLoader的機制保證了線程安全;不一樣的是,餓漢式在Singleton類被加載時(從代碼段3-2的Class.forName可見)就建立了一個實例對象,而靜態內部類即便Singleton類被加載也不會建立單例對象,除非調用裏面的getInstance()方法。由於當Singleton類被加載時,其靜態內部類SingletonHolder沒有被主動使用。只有當調用getInstance方法時,纔會裝載SingletonHolder類,從而實例化單例對象。
這樣,經過靜態內部類的方法就實現了lazy loading,很好地將懶漢式和餓漢式結合起來,既實現延遲加載,保證系統性能,也能保證線程安全。
然而,對於上述四種方式的單例模式,若是你的Singleton類實現了Serializable序列化接口,那麼可能會被序列化生成多個實例,由於readObject()方法一直返回一個新的對象:
1 2 3 4 5 6 |
|
這種狀況能夠經過在Singleton類添加readResolve()方法來解決:
1 2 3 4 |
|
可是這種解決方案雖解決了序列化的問題,可是沒法避免被反射。下面還有一種枚舉單例,寫法簡單,還能夠避免序列化、反射的問題。
枚舉單例
上面說到的靜態內部類方式不失爲一個高級的單例模式實現。但若是開發要求更嚴格一些,好比你的Singleton類實現了序列化,又或者想避免經過反射來破解單例模式的話,單例模式還能夠有另外一種形式。那就是枚舉單例。枚舉類型在JDK1.5被引進。這種方式也是《Effective Java》做者Josh Bloch 提倡的方式,它不只能避免多線程的問題,並且還能防止反序列化從新建立新的對象、防止被反射攻擊。代碼以下:
代碼5-1
1 2 3 4 5 6 7 8 9 10 11 |
|
在外部,能夠經過EnumSingleton.INSTANCE.work()來調用work方法。默認的枚舉實例的建立是線程安全的,可是實例內的各類方法則須要程序員來保證線程安全。總的來講,使用枚舉單例模式,有三個好處:1.實例的建立線程安全,確保單例。2.防止被反射建立多個實例。3.沒有序列化的問題。