單例模式的概念
單例模式(Singleton Pattern)的定義爲:確保一個類只有一個實例,並且自行實例化並向整個系統提供這個實例。java
單例模式是建立型模式。單例模式分爲餓漢式單例和懶漢式單例,接下來咱們對這兩種類型作詳細介紹。安全
餓漢式
餓漢式單例模式就是在類加載的時候就當即初始化,而且建立單例對象。無論你有沒有用到,都先建好了再說。它絕對線程安全,在線程還沒出現之前就實例化了,不可能存在訪問安全問題。微信
優勢:線程安全,沒有加任何鎖、執行效率比較高。架構
缺點:類加載的時候就初始化,無論後期用不用都佔着空間,浪費了內存。性能
餓漢式單例的寫法很簡單,看下面代碼:學習
還能夠經過靜態代碼塊的機制來實現:測試
這兩種寫法都很簡單,都是建立了一個餓漢式的單例類。優化
餓漢式單例適合用在單例類比較少的狀況下,在實際項目中,有可能會存在不少的單例類,若是咱們都使用餓漢式單例的話,對內存的浪費會很大,因此,咱們要學習更優的寫法。spa
懶漢式
懶漢式,顧名思義就是實例在用到的時候纔去建立,「比較懶」,用的時候纔去檢查有沒有實例,若是有則直接返回,沒有則新建。.net
下面看懶漢式的簡單實現:
上面這種寫法有必定機率會生成不一樣的對象,意味着這種寫法不是線程安全的。緣由以下:
假若有兩個線程,線程A和線程B同時走到「if(lazy == null)」這個判斷,由於lazy尚未沒實例化過,因此兩個線程判斷的結果都是true,而後同時進入 if 代碼塊執行 new 操做,這時,線程A建立了一個實例,線程B也建立了一個實例,最後 return 的 lazy 確定不是同一個對象,因此,這種寫法是線程不安全的。
那咱們要如何解決這個線程不安全的問題呢?最容易想到的方法就是加鎖。咱們把 getInstance() 方法進行加鎖,看代碼:
因爲咱們給該方法加上了 synchronized 鎖,因此當線程A進入 getInstance() 方法的時候,線程B就只能在方法外等到線程A執行完這個方法以後才能進入該方法。因爲線程A已經執行完該方法,因此此時 lazy 是不爲null的,線程B就不會進入 if 代碼塊,最後返回的確定是線程A建立的實例。
上面這種方式成功解決了線程安全問題,但在線程數量比較多的狀況下,大量線程會阻塞在方法外部,致使程序性能降低。爲了兼顧性能和線程安全問題,咱們能夠經過雙重檢查鎖的方式來建立懶漢式的單例:
當第一個線程調用 getInstance()方法時,第二個線程也能夠調用。當第一個線程執行到 synchronized 時會上鎖,第二個線程就會變成 MONITOR 狀態,出現阻塞。此時,阻塞並非基於整 個 LazySimpleSingleton 類的阻塞,而是在 getInstance()方法內部的阻塞,只要邏輯不太複雜,對於 調用者而言感知不到。
可是,用到 synchronized 關鍵字總歸要上鎖,對程序性能仍是存在必定影響的。難道沒有更好的方案嗎?顯示是有的,咱們能夠從類初始化的角度來考慮,採用靜態內部類的方式。看下面的代碼:
這種方式兼顧了餓漢式單例模式的內存浪費問題和 synchronized 的性能問題。內部類必定是要在 方法調用以前初始化,巧妙地避免了線程安全問題。
反射破壞單例
餓漢式單例和懶漢式單例都是將構造方法私有化,防止在外部經過 new 來建立對象實例,以達到保證全局只有一個實例的效果。那咱們思考一個問題:若是咱們經過反射來調用其構造方法,在調用 getInstance() 方法獲得 new 出來的實例,應該會存在兩個不一樣的實例。如今來看一段測試代碼,以上面的 LazyInnerClassSingleton 類爲例:
爲了防止這種狀況的發生,咱們在其構造方法中作一些限制,一旦出現屢次建立,則直接拋出異常:
至此,最牛B的單例模式的實現就完成了!
序列化破壞單例
一個單例對象建立好後,有時候咱們須要將對象序列化而後寫入磁盤,下次使用時再從磁盤中讀取對象 並進行反序列化,將其轉化爲內存對象。反序列化後的對象會從新分配內存,即從新建立。若是序列化 的目標對象爲單例對象,就違背了單例模式的初衷,至關於破壞了單例。來看一段代碼:
看下測試代碼:
運行結果爲false。從運行結果能夠看出,反序列化後的對象和手動建立的對象是不一致的,實例化了兩次,違背了單例模式的設計初衷。那麼,咱們如何保證在序列化的狀況下也可以實現單例模式呢?其實很簡單,只須要增長 readResolve() 方法便可。來看優化後的代碼:
這時,再執行上面的測試代碼,輸出結果就是 true 了。這是什麼緣由呢?咱們就要看JDK反序列化的源碼了。咱們進入 ObjectInputStream 類的 readObject()方法, 代碼以下:
咱們發現,在 readObject() 方法中調用了 readObject0() 方法。進入 readObject0() 方法,代碼以下:
咱們看到 TC_OBJECT 中調用了 ObjectInputStream 的 readOrdinaryObject()方法,看源碼:
咱們發現調用了 ObjectStreamClass 的 isInstantiable()方法,而 isInstantiable()方法的代碼以下:
上述代碼很是簡單,就是判斷一下構造方法是否爲空,構造方法不爲空就返回 true。這意味着只要 有無參構造方法就會實例化。這時候其實尚未找到加上 readResolve()方法就避免了單例模式被破壞的真正緣由。再回到 ObjectInputStream 的 readOrdinaryObject()方法,繼續往下看:
判斷無參構造方法是否存在以後,又調用了 hasReadResolveMethod() 方法,來看代碼:
上述代碼邏輯很是簡單,就是判斷 readResolveMethod 是否爲空,不爲空就返回 true。那麼 readResolveMethod 是在哪裏賦值的呢?經過全局查找知道,在私有方法 ObjectStreamClass()中給 readResolveMethod 進行了賦值,來看代碼:
上面的邏輯其實就是經過反射找到一個無參的 readResolve()方法,而且保存下來。如今回到 ObjectInputStream 的 readOrdinaryObject()方法繼續往下看,若是 readResolve()方法存在則調用 invokeReadResolve()方法,來看代碼:
咱們能夠看到,在 invokeReadResolve()方法中用反射調用了 readResolveMethod 方法。最終返回咱們添加的 readResolve() 方法中的實例。
雖然增長 readResolve()方法返回實例解決了單例模式被破壞的 問題,可是實際上實例化了兩次,只不過新建立的對象沒有被返回而已。
註冊式單例模式
註冊式單例模式又叫登記式單例模式,就是將每個實例都登記到某一個地方,使用惟一的標識 獲取實例。註冊式單例模式有兩種:一種爲枚舉式單例模式,另外一種爲容器式單例模式。
枚舉式單例模式
先來看枚舉式單例模式的寫法:
使用序列化與反序列化,看兩次拿到的對象是否同樣:
運行以後,看到結果返回時true,說明序列化不會對枚舉類型的單例產生破壞。那發射呢?咱們看下測試反射破壞枚舉單例的代碼:
運行以後,會報一個異常:java.lang.NoSuchMethodException。意思是沒找到無參的構造方法。這時候, 咱們打開 java.lang.Enum 的源碼,查看它的構造方法,只有一個 protected 類型的構造方法,代碼以下:
那咱們再來作一個下面這樣的測試:
運行以後,會報一個這樣的錯:「Cannot reflectively create enum objects」,即不能用反射來建立枚舉類型。其緣由是JDK源碼中,newInstance() 方法中作了判斷,若是修飾符是 Modifier.ENUM 枚舉類型,則直接拋出異常:
容器式單例
容器式單例的寫法以下:
容器式單例模式適用於實例很是多的狀況,便於管理。但它是非線程安全的。
點個關注吧,天天定時爲您送上熱騰騰的乾貨~~
本文分享自微信公衆號 - Java架構成長之路(K469785635)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。