摘要:所以,在版本前,雙重檢查鎖形式的單例模式是無法保證線程安全的。
單例模式可能是代碼最少的模式了,但是少不一定意味著簡單,想要用好、用對單例模式,還真得費一番腦筋。本文對Java中常見的單例模式寫法做了一個總結,如有錯漏之處,懇請讀者指正。
餓漢法
顧名思義,餓漢法就是在第一次引用該類的時候就創建對象實例,而不管實際是否需要創建。代碼如下:
public class Singleton {
private static Singleton = new Singleton(); private Singleton() {} public static getSignleton(){ return singleton; }
}
這樣做的好處是編寫簡單,但是無法做到延遲創建對象。但是我們很多時候都希望對象可以盡可能地延遲加載,從而減小負載,所以就需要下面的懶漢法:
懶漢法單線程寫法這種寫法是最簡單的,由私有構造器和一個公有靜態工廠方法構成,在工廠方法中對singleton進行null判斷,如果是null就new一個出來,最后返回singleton對象。這種方法可以實現延時加載,但是有一個致命弱點:線程不安全。如果有兩條線程同時調用getSingleton()方法,就有很大可能導致重復創建對象。
public class Singleton { private static Singleton singleton = null; private Singleton(){} public static Singleton getSingleton() { if(singleton == null) singleton = new Singleton(); return singleton; } }
考慮線程安全的寫法
這種寫法考慮了線程安全,將對singleton的null判斷以及new的部分使用synchronized進行加鎖。同時,對singleton對象使用volatile關鍵字進行限制,保證其對所有線程的可見性,并且禁止對其進行指令重排序優化。如此即可從語義上保證這種單例模式寫法是線程安全的。注意,這里說的是語義上,實際使用中還是存在小坑的,會在后文寫到。
public class Singleton { private static volatile Singleton singleton = null; private Singleton(){} public static Singleton getSingleton(){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } return singleton; } }
兼顧線程安全和效率的寫法
雖然上面這種寫法是可以正確運行的,但是其效率低下,還是無法實際應用。因為每次調用getSingleton()方法,都必須在synchronized這里進行排隊,而真正遇到需要new的情況是非常少的。所以,就誕生了第三種寫法:
public class Singleton { private static volatile Singleton singleton = null; private Singleton(){} public static Singleton getSingleton(){ if(singleton == null){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }
這種寫法被稱為“雙重檢查鎖”,顧名思義,就是在getSingleton()方法中,進行兩次null檢查。看似多此一舉,但實際上卻極大提升了并發度,進而提升了性能。為什么可以提高并發度呢?就像上文說的,在單例中new的情況非常少,絕大多數都是可以并行的讀操作。因此在加鎖前多進行一次null檢查就可以減少絕大多數的加鎖操作,執行效率提高的目的也就達到了。
坑
-
那么,這種寫法是不是絕對安全呢?前面說了,從語義角度來看,并沒有什么問題。但是其實還是有坑。說這個坑之前我們要先來看看volatile這個關鍵字。其實這個關鍵字有兩層語義。第一層語義相信大家都比較熟悉,就是可見性。可見性指的是在一個線程中對該變量的修改會馬上由工作內存(Work Memory)寫回主內存(Main Memory),所以會馬上反應在其它線程的讀取操作中。順便一提,工作內存和主內存可以近似理解為實際電腦中的高速緩存和主存,工作內存是線程獨享的,主存是線程共享的。volatile的第二層語義是禁止指令重排序優化。大家知道我們寫的代碼(尤其是多線程代碼),由于編譯器優化,在實際執行的時候可能與我們編寫的順序不同。編譯器只保證程序執行結果與源代碼相同,卻不保證實際指令的順序與源代碼相同。這在單線程看起來沒什么問題,然而一旦引入多線程,這種亂序就可能導致嚴重問題。volatile關鍵字就可以從語義上解決這個問題。
注意,前面反復提到“從語義上講是沒有問題的”,但是很不幸,禁止指令重排優化這條語義直到jdk1.5以后才能正確工作。此前的JDK中即使將變量聲明為volatile也無法完全避免重排序所導致的問題。所以,在jdk1.5版本前,雙重檢查鎖形式的單例模式是無法保證線程安全的。
靜態內部類法那么,有沒有一種延時加載,并且能保證線程安全的簡單寫法呢?我們可以把Singleton實例放到一個靜態內部類中,這樣就避免了靜態實例在Singleton類加載的時候就創建對象,并且由于靜態內部類只會被加載一次(JVM機制),所以這種寫法也是線程安全的:
public class Singleton { private static class Holder { private static Singleton singleton = new Singleton(); } private Singleton(){} public static Singleton getSingleton(){ return Holder.singleton; } }
但是,上面提到的所有實現方式都有兩個共同的缺點:
都需要額外的工作(Serializable、transient、readResolve())來實現序列化,否則每次反序列化一個序列化的對象實例時都會創建一個新的實例。
可能會有人使用反射強行調用我們的私有構造器(如果要避免這種情況,可以修改構造器,讓它在創建第二個實例的時候拋異常)。
枚舉寫法當然,還有一種更加優雅的方法來實現單例模式,那就是枚舉寫法:
public enum Singleton {
INSTANCE; private String name; public String getName(){ return name; } public void setName(String name){ this.name = name; }
}
使用枚舉除了線程安全和防止反射強行調用構造器之外,還提供了自動序列化機制,防止反序列化的時候創建新的對象。因此,Effective Java推薦盡可能地使用枚舉來實現單例。
總結
這篇文章發出去以后得到許多反饋,這讓我受寵若驚,覺得應該再寫一點小結。代碼沒有一勞永逸的寫法,只有在特定條件下最合適的寫法。在不同的平臺、不同的開發環境(尤其是jdk版本)下,自然有不同的最優解(或者說較優解)。
比如枚舉,雖然Effective Java中推薦使用,但是在Android平臺上卻是不被推薦的。在這篇Android Training中明確指出:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
再比如雙重檢查鎖法,不能在jdk1.5之前使用,而在Android平臺上使用就比較放心了(一般Android都是jdk1.6以上了,不僅修正了volatile的語義問題,還加入了不少鎖優化,使得多線程同步的開銷降低不少)。
最后,不管采取何種方案,請時刻牢記單例的三大要點:
線程安全
延遲加載
序列化與反序列化安全
參考資料
《Effective Java(第二版)》
《深入理解Java虛擬機——JVM高級特性與最佳實踐(第二版)》
轉載出自https://www.cnblogs.com/andy-...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/74294.html
摘要:使用靜態類體現的是基于對象,而使用單例設計模式體現的是面向對象。二編寫單例模式的代碼編寫單例模式的代碼其實很簡單,就分了三步將構造函數私有化在類的內部創建實例提供獲取唯一實例的方法餓漢式根據上面的步驟,我們就可以輕松完成創建單例對象了。 前言 只有光頭才能變強 回顧前面: 給女朋友講解什么是代理模式 包裝模式就是這么簡單啦 本來打算沒那么快更新的,這陣子在刷Spring的書籍。在看...
摘要:如果需要防范這種攻擊,請修改構造函數,使其在被要求創建第二個實例時拋出異常。單例模式與單一職責原則有沖突。源碼地址參考文獻設計模式之禪 定義 單例模式是一個比較簡單的模式,其定義如下: 保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。 或者 Ensure a class has only one instance, and provide a global point of ac...
摘要:總結我們主要介紹到了以下幾種方式實現單例模式餓漢方式線程安全懶漢式非線程安全和關鍵字線程安全版本懶漢式雙重檢查加鎖版本枚舉方式參考設計模式中文版第二版設計模式深入理解單例模式我是一個以架構師為年之內目標的小小白。 初遇設計模式在上個寒假,當時把每個設計模式過了一遍,對設計模式有了一個最初級的了解。這個學期借了幾本設計模式的書籍看,聽了老師的設計模式課,對設計模式算是有個更進一步的認識。...
摘要:關于對于重排序的講解,強烈推薦閱讀程曉明寫的深入理解內存模型二重排序。語義語義單線程下,為了優化可以對操作進行重排序。編譯器和處理器為單個線程實現了語義,但對于多線程并不實現語義。雙重加載的單例模式分析即雙重檢查加鎖。 版權聲明:本文由吳仙杰創作整理,轉載請注明出處:https://segmentfault.com/a/1190000009231182 1. 引言 在開始分析雙重加鎖單...
摘要:總之,選擇單例模式就是為了避免不一致狀態,避免政出多頭。二餓漢式單例餓漢式單例類在類初始化時,已經自行實例化靜態工廠方法餓漢式在類創建的同時就已經創建好一個靜態的對象供系統使用,以后不再改變,所以天生是線程安全的。 概念: Java中單例模式是一種常見的設計模式,單例模式的寫法有好幾種,這里主要介紹兩種:懶漢式單例、餓漢式單例。 單例模式有以下特點: 1、單例類只能有一個實例。 ...
摘要:一般來說,這種單例實現有兩種思路,私有構造器,枚舉。而這種方式又分了飽漢式,餓漢式。通過關鍵字防止指令重排序。什么是單例?為什么要用單例? 一個類被設計出來,就代表它表示具有某種行為(方法),屬性(成員變量),而一般情況下,當我們想使用這個類時,會使用new關鍵字,這時候jvm會幫我們構造一個該類的實例。而我們知道,對于new這個關鍵字以及該實例,相對而言是比較耗費資源的。所以如果我們能夠想...
閱讀 812·2021-11-18 10:02
閱讀 2503·2021-11-11 16:54
閱讀 2750·2021-09-02 09:45
閱讀 653·2019-08-30 12:52
閱讀 2774·2019-08-29 14:04
閱讀 2745·2019-08-29 12:39
閱讀 447·2019-08-29 12:27
閱讀 1887·2019-08-26 13:23