通常我們在寫程序的時候會碰到一個類只允許在整個系統中只存在一個實例(Instance)? 的情況, 比如說我們想做一計數器,統計某些接口調用的次數,通常我們的數據庫連接也是只期望有一個實例。Windows系統的系統任務管理器也是始終只有一個,如果你打開了windows管理器,你再想打開一個那么他還是同一個界面(同一個實例), 還有比如 做.Net平臺的人都知道,AppDomain 對象,一個系統中也只有一個,所有的類庫都會加載到AppDomain中去運行。只需要一個實例對象的場景,隨處可見,那么有么有什么好的解決方法來應對呢? 有的,那就是 單例模式。
一、單例模式定義
單例模式(Singleton Pattern):確保某一個類只有一個實例,而且自行實例化并向整個系統提供這個實例,這個類稱為單例類,它提供全局訪問的方法。單例模式是一種對象創建型模式。
二、單例模式結構圖
- Singleton(單例):在單例類的內部實現只生成一個實例,同時它提供一個靜態的GetInstance()工廠方法,讓客戶可以訪問它的唯一實例;為了防止在外部對其實例化,將其構造函數設計為私有(private);在單例類內部定義了一個Singleton類型的靜態對象,作為外部共享的唯一實例。
三、 單例模式典型代碼
public class Singleton {private static Singleton instance;private Singleton(){}public static Singleton GetInstance(){if(instance==null){instance=new Singleton();}return instance;} }
客戶端調用代碼:
static void Main(string[] args) {Singleton singleto = Singleton.GetInstance(); }
在C#中經常將統一訪問點暴露出一個只讀的屬性供客戶端程序使用,這樣代碼就變成了這樣:
public class Singleton {private static Singleton instance;private Singleton(){}public static Singleton GetInstance{get{if (instance == null){instance = new Singleton();}return instance;}} }
客戶端調用:
static void Main(string[] args) {Singleton singleton = Singleton.GetInstance; }
四、單例模式實例
1. 懶漢模式
假如我們要做一個程序計數器,一旦程序啟動無論多少個客戶端調用這個 計數器計數的結果始終都是在前一個的基礎上加1,那么這個計數器類就可以設計成一個單例模式的類。
public class SingletonCounter {private static SingletonCounter instance;private static int number=0;private SingletonCounter() { }public static SingletonCounter Instance{get{if (instance == null) instance = new SingletonCounter();number++;return instance;}}public int GetCounter(){return number;} }
客戶端調用:
static void Main(string[] args) {//App A call the counter;SingletonCounter singletonA = SingletonCounter.Instance;int numberA = singletonA.GetCounter();Console.WriteLine("App A call the counter get number was:" + numberA);//App B call the counter;SingletonCounter singletonB = SingletonCounter.Instance;int numberB = singletonA.GetCounter();Console.WriteLine("App B call the counter get number was:" + numberB);Console.ReadKey(); }
輸出結果:
這個實現是線程不安全的,如果有多個線程同時調用,并且又恰恰在計數器初始化的瞬間多個線程同時檢測到了 instance==null為true情況,會怎樣呢?這就是下面要討論的 “加鎖懶漢模式”
2、加鎖懶漢模式
多個線程同時調用并且同時檢測到 instance == null 為 true的情況,那后果就是會出現多個實例了,那么就無法保證唯一實例了,解決這個問題就是增加一個對象鎖來確保在創建的過程中只有一個實例。(鎖可以確保鎖住的代碼塊是線程獨占訪問的,如果一個線程占有了這個鎖,其它線程只能等待該線程釋放鎖以后才能繼續訪問)。
public class SingletonCounter {private static SingletonCounter instance;private static readonly object locker = new object();private static int number = 0;private SingletonCounter() { }public static SingletonCounter Instance{get{lock (locker){if (instance == null) instance = new SingletonCounter();number++;return instance;}}}public int GetCounter(){return number;} }
客戶端調用代碼:
static void Main(string[] args) { for (int i = 1; i < 100; i++){var task = new Task(() =>{SingletonCounter singleton = SingletonCounter.Instance;int number = singleton.GetCounter();Console.WriteLine("App call the counter get number was:" + number);});task.Start();}Console.ReadKey(); }
輸出結果:
這種模式是線程安全,即使在多線程的情況下仍然可以保持單個實例。那么這種模式會不會有什么問題呢?假如系統的訪問量非常大,并發非常高,那么計數器就會是一個性能瓶頸,因為對鎖會使其它的線程無法訪問。在訪問量不大,并發量不高的系統尚可應付,如果高訪問量,高并發的情況下這樣做肯定是不行的,那么有什么辦法改進呢?這就是下面要討論的“雙檢查加鎖懶漢模式”。
3、雙檢查加鎖懶漢模式
加鎖懶漢模式雖然保證了系統的線程安全,但是卻為系統帶來了新能問題,主要的性能來自鎖帶來開銷,雙檢查就是解決這個鎖帶來的問題,在鎖之前再做一次 instance==null的檢查,如果返回true就直接返回 單例對象了,避開了無謂的鎖, 我們來看下,雙檢查懶漢模式代碼:
public class DoubleCheckLockSingletonCounter {private static DoubleCheckLockSingletonCounter instance;private static readonly object locker = new object();private static int number = 0;private DoubleCheckLockSingletonCounter() { }public static DoubleCheckLockSingletonCounter Instance{get{if (instance == null){lock (locker){if (instance == null){instance = new DoubleCheckLockSingletonCounter();}}}number++;return instance;}}public int GetCounter(){return number;} }
客戶端調用代碼和“懶漢加鎖模式”相同,輸出結果也相同。
4、餓漢模式
單例模式除了我們上面講的三種懶漢模式外,還有一種叫“餓漢模式”的實現方式,“餓漢模式”直接在Singleton類里實例化了當前類的實例,并且保存在一個靜態對象中,因為是靜態對象,所以在程序啟動的時候就已經實例化好了,后面直接使用,因此不存在線程安全的問題。
下面是“餓漢模式”的代碼實現:
public class EagerSingletonCounter {private static EagerSingletonCounter instance = new EagerSingletonCounter();private static int number = 0;private EagerSingletonCounter() { }public static EagerSingletonCounter Instance{get{number++;return instance;}}public int GetCounter(){return number;} }
?
五、單例模式應用場景
單例模式只有一個角色非常簡單,使用的場景也很明確,就是一個類只需要、且只能需要一個實例的時候使用單例模式。
六、擴展
?
1、”餓漢模式“和”懶漢模式“的比較
”餓漢模式“在程序啟動的時候就已經實例化好了,并且一直駐留在系統中,客戶程序調用非常快,因為它是靜態變量,雖然完美的保證線程的安全,但是如果創建對象的過程很復雜,要占領系統或者網絡的一些昂貴的資源,但是在系統中使用的頻率又極低,甚至系統運行起來后都不會去使用該功能,那么這樣一來,啟動之后就一直占領著系統的資源不釋放,這有些得不償失。
“懶漢模式“ 恰好解決了”餓漢模式“這種占用資源的問題,”懶漢模式”將類的實例化延遲到了運行時,在使用時的第一次調用時才創建出來并一直駐留在系統中,但是為了解決線程安全問題, 使用對象鎖也是 影響了系統的性能。這兩種模式各有各的好處,但是又各有其缺點。
有沒有一種折中的方法既可以避免一開始就實例化且一直占領系統資源,又沒有性能問題的Singleton呢? 答案是:有的。
2、第三種選擇
“餓漢模式“類不能實現延遲加載,不管用不用始終占據內存;”懶漢式模式“類線程安全控制煩瑣,而且性能受影響。我們用一種折中的方法來解決這個問題,針對主要矛盾, 即:既可以延時加載又不影響性能。
在Singleton的內部創建一個私有的靜態類用于充當單例類的”初始化器“,專門用來創建Singleton的實例:
public class BestPracticeSingletonCounter {private static class SingletonInitializer{public static BestPracticeSingletonCounter instance = new BestPracticeSingletonCounter();} private static int number = 0;private BestPracticeSingletonCounter() { }public static BestPracticeSingletonCounter Instance{get{number++;return SingletonInitializer.instance;}}public int GetCounter(){return number;} }
這種模式兼具了”餓漢“和”懶漢“模式的優點有摒棄了其缺點,可以說是一個完美的實現。