文章目錄
- 簡介
- 不安全線程的單例模式
- 簡單安全線程帶鎖
- 雙重檢查 - 帶鎖
- 安全初始化
- 安全並且懶漢式靜態初始化
- 帶泛型的懶漢式單例
- 異常
- 提高效率
- 總結
簡介
單例模式是軟件工程中廣為人知的設計模式。單例模式就是指一個永遠只能實例化一次。使用的方式是調用類里創建的靜態方法。通常來說,單例模式創建的類,都是不帶形參的 ,原因就是當創建多個實例的時候,如果參數不同的話(比如2個不同的重載構造函數),那么就會造成一些不必要的問題(如果相同的實例要被創建而且他們使用相同的參數的話,那么建議使用工廠模式),這篇文章的定位就是沒有 任何的參數的情況下,通常情況下,單例模式是LAZY的,也就是說相當的容易創建。
在C#中實現單例模式有很多種方式。我將在下面以上面的目錄的形式呈現給大家,開始我會跟大家介紹最常用的單例模式的寫法,這些寫法的線程並不安全,然后會提到懶漢式寫法(Lazy-Load),然后就是線程安全,最后會跟大家介紹一下提高效率的方式。
所有的實現將會用通俗的語言來介紹,但是要注意以下:
- 只有一個構造函數,而且這個構造函數是私有的,不帶任何的參數的。這是為了防止其他的類實例化這個單例類(這也許違反了設計模式),注意這種方式也會阻止子類,如果單例能被子類實例化一次,那么也可以被子類實例化多次。如果單例的每個子類都可以創建實例的話,就違反了單例模式。如果你需要父類(基類)的單例的實現的話我覺得工廠模式是可以有用到的,但是具體的類型不會被知道直到運行時(CLR)開始。
- 類是sealed的,也就是封閉的。這是沒有必要的,嚴格意義上說,只是為了JIT提高效率。
- 靜態變量 ,類型被定義為這個單例類的一個引用。
- 建立一個公共的而且靜態的方法,返回值是這個單例類,如果有需要的話。
注意所有的實現都是用一個公共的且靜態的屬性作為實例的入口。在所有的情況下屬性可以方便的轉換成方法,而且和線程安全或者效率不沖突。
第一個版本 - 非線程安全
// 不要用這種方式 public sealed class Singleton { private static Singleton instance=null; private Singleton() { } public static Singleton Instance { get { if (instance==null) { instance = new Singleton(); } return instance; } } }
我要表明的是,上面的方法是非線程安全的,2個不同的線程可以同時進入這個方法,如果instance為空的並且這里返回真的情況下,都可以創建實例,這顯然違反了單例模式,實際上,在測試以前,實例就已經有可能被創建了,但是內存模型不能保證這個實例能被其他的線程看到,除非合適的內存屏障已經被跨過了。
第二個版本 - 簡單安全線程
public sealed class Singleton { private static Singleton instance = null; private static readonly object padlock = new object(); Singleton() { } public static Singleton Instance { get { lock (padlock) { if (instance == null) { instance = new Singleton(); } return instance; } } } }
上述實現是線程安全的。這個線程在共享的object上取出了一把鎖,然后在創建實例以前檢查這個實例是否被創建了。這個保護了內存屏障問題(lock保證了所有的讀取操作是在LOCK獲得以后發生的,所有的unlock保證了所有的寫操作在lock 釋放以后發生的),這樣就保證了一個線程只能創建一個實例(每次只有一個線程在這段代碼中運行),不巧的是,性能上來說,鎖變成了每次都必須的當這個實例被響應的時候。
注意除了在鎖當中鎖住typeof(Singleton)這種類型的以外,我鎖住了一個靜態私有的變量,對於這個類來說。如果是鎖 的一個對象的話,其他的類可以進入並且鎖住(比如Type)這樣會造成性能風險的問題甚至死鎖。這是我的大體偏好 - 也許可能的話,只有鎖住對象才能達到最終的目的,或者某些文章說鎖是為了達到一些特別的目的。(比如等待或者脈沖一個隊列)。通常來說這樣的對象應該被設置成私有的。這樣會讓寫線程安全更加的容易。
第三個版本 - 嘗試線程安全(雙重鎖定)
public sealed class Singleton { private static Singleton instance = null; private static readonly object padlock = new object(); Singleton() { } public static Singleton Instance { get { if (instance == null) { lock (padlock) { if (instance == null) { instance = new Singleton(); } } } return instance; } } }
這個實現嘗試了線程安全,當然並沒必要每次都要取出lock,但是這種方式有如下4個缺點:
- 這種方式JAVA中是無效的。也許你會認為我這是在說廢話,但是我覺得這是值得你去了解的。JAVA內存模型中新對象的引用被分配到實例之前並不能保證構造函數完成初始化,JAVA 內存模型重新工作(在1.5版本中),但是雙重檢查鎖依然是壞的,在不帶volatile 的變量(比如C#)。
- 不帶任何的記憶屏障,在ECMA CLI規格中也是破碎的。也許在.NET 2.0以下是安全的,但是我更傾向於不依賴於哪些強類型的語義,特別的說,對於安全性來說這是值得懷疑的。如果把變量變成 volatile的是可以運行的,明確的內存屏障會進行響應,雖然后面有些情況專家都不能完全同意屏障是必須的。我打算嘗試避免站在對的或者錯誤的立場上去回答這個問題!
- 容易報錯。這種模式要像上面一樣的精確 - 任何的大的改動都會造成正確性和性能方面的沖擊。
- 依然沒有后面說的那種寫法好。
第四個版本 - 不完全lazy,但是線程安全且不用用鎖
public sealed class Singleton { private static readonly Singleton instance = new Singleton(); // 顯示的static 構造函數 //沒必要標記類型 - 在field初始化以前 static Singleton() { } private Singleton() { } public static Singleton Instance { get { return instance; } } }
未完待續,原文鏈接地址:
http://csharpindepth.com/Articles/General/Singleton.aspx#unsafe