複製來自 http://blog.csdn.net/cselmu9/article/details/51366946java
在全部的設計模式中,單例模式是咱們在項目開發中最爲常見的設計模式之一,而單例模式有不少種實現方式,你是否都瞭解呢?高併發下如何保證單例模式的線程安全性呢?如何保證序列化後的單例對象在反序列化後任然是單例的呢?這些問題在看了本文以後都會一一的告訴你答案,趕快來閱讀吧!mysql
在文章開始以前咱們仍是有必要介紹一下什麼是單例模式。單例模式是爲確保一個類只有一個實例,併爲整個系統提供一個全局訪問點的一種模式方法。sql
從概念中體現出了單例的一些特色:數據庫
(1)、在任何狀況下,單例類永遠只有一個實例存在編程
(2)、單例須要有能力爲整個系統提供這一惟一實例 設計模式
爲了便於讀者更好的理解這些概念,下面給出這麼一段內容敘述:緩存
在計算機系統中,線程池、緩存、日誌對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。這些應用都或多或少具備資源管理器的功能。每臺計算機能夠有若干個打印機,但只能有一個Printer Spooler,以免兩個打印做業同時輸出到打印機中。每臺計算機能夠有若干通訊端口,系統應當集中管理這些通訊端口,以免一個通訊端口同時被兩個請求同時調用。總之,選擇單例模式就是爲了不不一致狀態,避免政出多頭。
正是因爲這個特色,單例對象一般做爲程序中的存放配置信息的載體,由於它能保證其餘對象讀到一致的信息。例如在某個服務器程序中,該服務器的配置信息可能存放在數據庫或文件中,這些配置數據由某個單例對象統一讀取,服務進程中的其餘對象若是要獲取這些配置信息,只需訪問該單例對象便可。這種方式極大地簡化了在複雜環境 下,尤爲是多線程環境下的配置管理,可是隨着應用場景的不一樣,也可能帶來一些同步問題。
安全
舒適提示:本文敘述中涉及到的相關源碼能夠在這裏進行下載源碼,讀者可免積分下載。服務器
餓漢式單例是指在方法調用前,實例就已經建立好了。下面是實現代碼:多線程
以上是單例的餓漢式實現,咱們來看看餓漢式在多線程下的執行狀況,給出一段多線程的執行代碼:
以上代碼運行結果:
從運行結果能夠看出實例變量額hashCode值一致,這說明對象是同一個,餓漢式單例實現了。
懶漢式單例是指在方法調用獲取實例時才建立實例,由於相對餓漢式顯得「不急迫」,因此被叫作「懶漢模式」。下面是實現代碼:
這裏實現了懶漢式的單例,可是熟悉多線程併發編程的朋友應該能夠看出,在多線程併發下這樣的實現是沒法保證明例實例惟一的,甚至能夠說這樣的失效是徹底錯誤的,下面咱們就來看一下多線程併發下的執行狀況,這裏爲了看到效果,咱們對上面的代碼作一小點修改:
這裏假設在建立實例前有一些準備性的耗時工做要處理,多線程調用:
執行結果以下:
從這裏執行結果能夠看出,單例的線程安全性並無獲得保證,那要怎麼解決呢?
要保證線程安全,咱們就得須要使用同步鎖機制,下面就來看看咱們如何一步步的解決 存在線程安全問題的懶漢式單例(錯誤的單例)。
出現非線程安全問題,是因爲多個線程能夠同時進入getInstance()方法,那麼只須要對該方法進行synchronized的鎖同步便可:
此時任然使用前面驗證多線程下執行狀況的MyThread類來進行驗證,將其放入到org.mlinge.s03包下運行,執行結果以下:
從執行結果上來看,問題已經解決了,可是這種實現方式的運行效率會很低。同步方法效率低,那咱們考慮使用同步代碼塊來實現:
這裏的實現可以保證多線程併發下的線程安全性,可是這樣的實現將所有的代碼都被鎖上了,一樣的效率很低下。
針對某些重要的代碼進行單獨的同步,而不是所有進行同步,能夠極大的提升執行效率,咱們來看一下:
此時一樣使用前面驗證多線程下執行狀況的MyThread類來進行驗證,將其放入到org.mlinge.s04包下運行,執行結果以下:
從運行結果來看,這樣的方法進行代碼塊同步,代碼的運行效率是可以獲得提高,可是卻沒能保住線程的安全性。看來還得進一步考慮如何解決此問題。
爲了達到線程安全,又能提升代碼執行效率,咱們這裏能夠採用DCL的雙檢查鎖機制來完成,代碼實現以下:
將前面驗證多線程下執行狀況的MyThread類放入到org.mlinge.s05包下運行,執行結果以下:
從運行結果來看,該中方法保證了多線程併發下的線程安全性。
這裏在聲明變量時使用了volatile關鍵字來保證其線程間的可見性;在同步代碼塊中使用二次檢查,以保證其不被重複實例化。集合其兩者,這種實現方式既保證了其高效性,也保證了其線程安全性。
DCL解決了多線程併發下的線程安全問題,其實使用其餘方式也能夠達到一樣的效果,代碼實現以下:
以上代碼就是使用靜態內置類實現了單例模式,這裏將前面驗證多線程下執行狀況的MyThread類放入到org.mlinge.s06包下運行,執行結果以下:
從運行結果來看,靜態內部類實現的單例在多線程併發下單個實例獲得了保證。
靜態內部類雖然保證了單例在多線程併發下的線程安全性,可是在遇到序列化對象時,默認的方式運行獲得的結果就是多例的。
代碼實現以下:
序列化與反序列化測試代碼:
運行以上代碼,獲得的結果以下:
從結果中咱們發現,序列號對象的hashCode和反序列化後獲得的對象的hashCode值不同,說明反序列化後返回的對象是從新實例化的,單例被破壞了。那怎麼來解決這一問題呢?
解決辦法就是在反序列化的過程當中使用readResolve()方法,單例實現的代碼以下:
再次運行上面的測試代碼,獲得的結果以下:
從運行結果可知,添加readResolve方法後反序列化後獲得的實例和序列化前的是同一個實例,單個實例獲得了保證。
靜態代碼塊中的代碼在使用類的時候就已經執行了,因此能夠應用靜態代碼塊的這個特性的實現單例設計模式。
測試代碼以下:
運行結果以下:
從運行結果看,單例的線程安全性獲得了保證。
枚舉enum和靜態代碼塊的特性類似,在使用枚舉時,構造方法會被自動調用,利用這一特性也能夠實現單例:
測試代碼以下:
執行後獲得的結果:
運行結果代表單例獲得了保證,可是這樣寫枚舉類被徹底暴露了,聽說違反了「職責單一原則」,那咱們來看看怎麼進行改造呢。
不暴露枚舉類實現細節的封裝代碼以下:
驗證單例實現的代碼以下:
驗證結果:
驗證結果代表,完善後的單例實現更爲合理。
以上就是本文要介紹的全部單例模式的實現,相信認真閱讀的讀者都已經明白文章開頭所引入的那幾個問題了,祝你們讀得開心:-D!
備註:本文的編寫思路和實例源碼參照《Java多線程編程核心技術》-(高洪巖)一書中第六章的學習案例撰寫。