設計模式之單例模式
Intro
一個類只允許創建唯一一個對象(或者實例),那這個類就是一個單例類,這種設計模式就叫作單例設計模式,簡稱單例模式。
單例模式可能是大家聽說最多的設計模式了,網上介紹最多的設計模式大概就是單例模式了,我看過的設計模式相關的文章很多都是寫一篇介紹單例模式,然後就沒有了。
經典的設計模式有 23 種, 如果隨便抓一個程序員,讓他說一說最熟悉的 3 種設計模式,那其中肯定會包含今天要講的單例模式,
使用場景
單例模式主要用來確保某個類型的實例只能有一個。比如手機上的藍牙之類的只能有一個的實例的場景可以考慮用單例模式。
主要作用:
- 處理資源訪問衝突,比如說上面說的系統唯一硬件,系統文件訪問衝突等
- 表示全局唯一類,比如系統中的唯一 id 生成器
單例模式的實現
單例模式的實現,通常需要私有化構造方法,防止外部類直接使用單例類的構造方法創建對象
簡單非線程安全的實現
public class Singleton
{
private static Singleton _instance;
private Singleton()
{
}
public static Singleton GetInstance()
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
這種方式比較簡單,但是不是線程安全的,多線程高併發情況下可能會導致創建多個實例,但是如果你的業務場景允許創建多個,我覺得問題也不大,如果一定要保證只能創建一個實例,可以參考下面的做法
雙檢鎖(懶漢式)
/// <summary>
/// 雙重判空加鎖,飽漢模式(懶漢式),用到的時候再去實例化
/// </summary>
public class Singleton
{
private static Singleton _instance;
private static readonly object SyncLock = new object();
private Singleton()
{
}
public static Singleton GetInstance()
{
if (_instance == null)
{
lock (SyncLock)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
}
這種方式的執行過程會先檢查是否完成了實例化,如果已經實例化則直接返回實例,如果沒有就嘗試獲取鎖,獲得鎖之後再判斷一下是否已經實例化,如果已經實例化則返回實例,如果沒有就進行實例化
靜態初始化(餓漢式)
/// <summary>
/// 餓漢模式-就是屌絲,擔心餓死。類加載就給準備好
/// </summary>
public sealed class Singleton1
{
/// <summary>
/// 靜態初始化,由 CLR 去創建,無需加鎖
/// </summary>
private static readonly Singleton1 Instance = new Singleton1();
private Singleton1()
{
}
public static Singleton1 GetInstance() => Instance;
}
這也是一種常見的實現單例模式的用法,但是這種方式就不支持懶加載了,不像上面那種方式可以做到需要的時候再實例化,適用於這個對象會被頻繁使用或者這個類比較小,是否實例化沒有什麼影響。
併發字典型
這個是之前忘記在哪裏看到的微軟框架裏的一段代碼,類似,可能和源碼並不完全一樣,只是提供一種實現思路
/// <summary>
/// 使用 ConcurrentDictionary 實現的單例方法,用到的時候再去實例化
/// 這種方式類似於第一種方式,只是使用了併發集合代替了雙重判斷和 lock
/// </summary>
public class Singleton2
{
private static readonly ConcurrentDictionary<int, Singleton2> Instances = new ConcurrentDictionary<int, Singleton2>();
private Singleton2()
{
}
public static Singleton2 GetInstance() => Instances.GetOrAdd(1, k => new Singleton2());
}
Lazy
C# 裏提供了 Lazy
的方式實現延遲實例化
/// <summary>
/// 使用 Lazy 實現的單例方法,用到的時候再去實例化
/// </summary>
public class Singleton3
{
private static readonly Lazy<Singleton3>
LazyInstance = new Lazy<Singleton3>
(() => new Singleton3());
private Singleton3()
{
}
public static Singleton3 GetInstance() => LazyInstance.Value;
}
其他
你也可以使用內部類等實現方式,這裏就不介紹了,想了解可以自己網上找一下
驗證是否線程安全,驗證示例代碼:
Console.WriteLine($"Singleton");
Enumerable.Range(1, 10).Select(i => Task.Run(() =>
{
Console.WriteLine($"{Singleton.GetInstance().GetHashCode()}");
})).WhenAll().Wait();
Console.WriteLine($"Singleton1");
Enumerable.Range(1, 10).Select(i => Task.Run(() =>
{
Console.WriteLine($"{Singleton1.GetInstance().GetHashCode()}");
})).WhenAll().Wait();
Console.WriteLine($"Singleton2");
Enumerable.Range(1, 10).Select(i => Task.Run(() =>
{
Console.WriteLine($"{Singleton2.GetInstance().GetHashCode()}");
})).WhenAll().Wait();
Console.WriteLine($"Singleton3");
Enumerable.Range(1, 10).Select(i => Task.Run(() =>
{
Console.WriteLine($"{Singleton3.GetInstance().GetHashCode()}");
})).WhenAll().Wait();
上面的 WhenAll
是一個擴展方法,就是調用的 Task.WhenAll
,輸出示例:
單例模式的存在的問題
- 單例對 OOP 特性的支持不友好,使用單例模式通常也就意味着放棄了 OOP 的繼承,多態特性
- 單例會隱藏類之間的依賴關係,單例模式,不允許顯示 new,使得對象的創建過程對外部來說是不可見的,內部有哪些依賴對外也是不可見的,這樣在系統重構的時候就會很危險,很容易造成系統出現問題
- 單例對代碼的擴展性不友好,單例類只能有一個對象實例。如果未來某一天,我們需要在代碼中創建兩個實例或多個實例,那就要對代碼有比較大的改動
- 單例對代碼的可測試性不友好,如果單例類依賴比較重的外部資源,比如 DB,我們在寫單元測試的時候,希望能通過 mock 的方式將它替換掉。而單例類這種硬編碼式的使用方式,導致無法實現 mock 替換
- 單例不支持有參數的構造函數,單例模式通常使用私有構造方法,而且只會調用一次構造方法,所以通常不支持構造方法參數,如果有參數通常會給調用方造成誤解,兩次調用傳遞的參數不一致的時候如何處理是一個問題
More
隨着現在依賴注入思想的普及,asp.net core 更是基於依賴框架構建的,使用依賴注入的方式可以較好的解決上面的各種問題
基於依賴注入框架,你可以不必擔心對象的創建和銷燬,讓依賴注入框架管理對象,這樣這個要實現單例模式的類型可以和其他普通類型一樣,只需要使用依賴注入框架註冊服務的時候指定服務生命週期爲單例即可,比如使用微軟的依賴注入框架的時候可以使用 services.AddSingleton<TSingletonService>();
來註冊單例服務