今天來談談設計模式中的單例模式,溫故知新,以免生疏。
軟體設計領域的四位世界級大師Gang Of Four (GoF):Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides四人合著了《Design Patterns - Elements of Reusable Object-Oriented Software》一書,(中文譯名:《設計模式:可複用物件導向軟體的基礎》)。該書首次提到了軟體開發中設計模式的概念,對物件導向軟體設計產生了巨大影響。
單例模式屬於建立型模式,那麼這裡就要簡述一下建立型模式。顧名思義,就是建立物件的設計模式。頻繁地使用基本物件建立方式(比如new操作)會使系統的耦合性變高,導致某些設計上的問題。建立型模式對類的範例化進行抽象,將物件的建立與物件的使用分離,隱藏了類的範例化過程。
在系統中,有一些物件其實只需要一個,例如執行緒池、快取、登入檔、紀錄檔物件、充當印表機顯示卡等裝置驅動程式的物件。同時這些類比較龐大複雜,並且這些物件完全可以複用。若建立多個範例或頻繁建立銷燬範例物件,會導致程式行為異常、資源使用過量、或者不一致性的結果,這就有了單例模式。
單例模式含義
確保一個類只有一個範例,並提供該範例的全域性存取點,後半句通俗點講,就是向整個系統提供這個範例。
建構函式: private Singleton(){}
,私有。因為一個類只能有一個範例,不可被外部再次範例化,構造方法不可能是public
,只能是private
。保證了不能通過構造器進行建立範例物件。
(成員)變數:private static Singleton instance
,私有,靜態變數。由於類中僅有一個範例,屬於當前類的靜態變數,外部無法直接存取。
方法:public static Singleton getInstance(){}
,公有,靜態方法。要向整個系統提供該單例,就要建立一個公有的靜態方法向外界提供當前類的範例。
1. 懶漢式(執行緒不安全)
if (Instance == null)
時,如果多個執行緒同時進入,並且此時Instance
為 null,那麼這些執行緒就會在執行new
語句,導致多次範例化物件,這是我們不希望看到的。public class Singleton {
private static Singleton instance; //宣告靜態變數
private Singleton (){} //構造器
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2. 懶漢式(執行緒安全)
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() { //使用同步方法
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3. 餓漢式(執行緒安全)
Instance
可被多次範例化,因此,在類載入時就直接範例化Instance
就可以保證執行緒安全問題。它基於 累載入機制避免了多執行緒的同步問題,不過這時候初始化instance
顯然沒有達到懶載入(lazy loading)的效果。public class Singleton {
private static Singleton instance = new Singleton(); //類載入時就進行範例化
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
注:懶漢式與餓漢式最主要的區別在於建立單例的時機不同,懶漢式根據是否需要範例,手動建立;餓漢式在類載入時自動建立單例
4. 雙重校驗鎖方式(執行緒安全)
描述:雙重校驗鎖(double-checked locking,DCL)也叫雙檢鎖,JDK1.5出現的功能。這種方式採用雙鎖機制,同時加鎖操作只需要對範例化那部分程式碼進行,只有當Instance
沒有被範例化時(Instance == null
) ,才需要進行加鎖。
優點:懶載入,多執行緒環境下可保證執行緒安全,效能較高。
缺點:相比前幾種方式,實現較為複雜。
雙重校驗鎖的完善過程:
getInstance()
時,存取的執行緒不需要競爭鎖,都可以直接進入。再進行下一步判斷,若此時範例物件還沒有被構建,執行緒開始競爭鎖,搶到鎖的執行緒開始建立單例。public class Singleton {
private static Singleton Instance;
private Singleton() {}
public static Singleton getInstance() {
if (Instance == null) {
synchronized (Singleton.class) { //在需要構建單例的時候給Class物件加鎖
Instance = new Singleton();
}
}
return Instance;
}
}
問題:在多個執行緒執行判斷條件時,雖然只有一個執行緒能夠搶到鎖取建立單例,但是可能有其他執行緒已經進入了if
程式碼塊,之後會再進行if
判斷了,而這些執行緒等待釋放鎖後,隨即又會建立範例物件,最終範例會被多次被建立。顯然執行緒不安全。
instance
已經被執行緒A初始化了,不可能等於null,直接退出,返回A執行緒建立的單例。public class Singleton {
private static Singleton Instance;
private Singleton() {}
public static Singleton getInstance() {
if (Instance == null) {
synchronized (Singleton.class) { //在需要構建單例的時候給Class物件加鎖
if (Instance == null) { //增加了判空條件
Instance = new Singleton();
}
}
}
return Instance;
}
}
Instance = new Singleton();
時,大致可以分為三步:1)給Instance
範例分配記憶體;2)初始化Instance
的構造器;3)將instance
物件指向分配的記憶體地址(這一步Instance
就非null了)。由於JVM為了優化指令,提高程式的執行效率,允許指令重排,導致在程式實際執行的時候,順序變為1)>> 3)>> 2),這在單執行緒的情況下是沒有問題的。但是,在多執行緒的環境下,執行緒有可能拿到一個尚未被初始化的範例,程式必然報錯。使用 volatile 關鍵字可以禁止 JVM 的指令重排,保證在多執行緒環境下也能正常執行。public class Singleton {
private volatile static Singleton Instance; //增加volatile關鍵字,防止JVM指令重排
private Singleton (){}
public static Singleton getInstance() {
if (Instance == null) {
synchronized (Singleton.class) {
if (Instance == null) {
Instance = new Singleton();
}
}
}
return singleton;
}
}
5.靜態內部類方式
Singleton
類載入時,靜態內部類 SingletonHolder
並沒有被載入進記憶體。只有當呼叫 getInstance()
方法從而觸發 SingletonHolder.INSTANCE
時 ,SingletonHolder
才會被載入,此時初始化INSTANCE
範例。實現了延遲載入。 這種方式只適用於靜態域的情況,雙檢鎖方式可在範例域需要延遲初始化時使用。public class Singleton {
private Singleton() {
}
//靜態內部類
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE; //存取靜態內部類中靜態成員
}
}
6.列舉方式
setAccessible()
方法可以將私有建構函式的存取級別設定為 public
,然後呼叫建構函式從而範例化物件。如果要防止這種攻擊,需要在建構函式中新增防止範例化第二個物件的程式碼。解決序列化和反射攻擊很麻煩,而列舉實現不會出現這兩種問題,因此說列舉實現單例模式式最佳實踐方法。private
方法,能夠防止多次範例化,是目前最安全的實現單例的方法。public enum Singleton {
INSTANCE;
public void whateverMethod() { //任意方法
}
}
一般情況下,不建議使用兩種懶漢式實現單例模式;明確使用靜態方法和實現懶載入效果時,會採用靜態內部類方式;涉及到反序列化建立物件的時候,可以使用列舉方式;一般而言,餓漢式以及雙重校驗鎖比較常用。
本文來自部落格園,作者:Joe__Bryant,轉載請註明原文連結:https://www.cnblogs.com/KobeForever/p/WhereAmazingHappens1.html