設計模式之單例模式
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;
}
其他
你也可以使用內部類, Interlocked 等實作方式,這里就不介紹了,想了解可以自己網上找一下
驗證是否執行緒安全,驗證示例代碼:
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>(); 來注冊單例服務
關于使用雙檢鎖實作單例的時候是否要使用 volatile 的問題,在 C# 如果你使用了 lock 就沒有必要再去用 volatile 標記要同步的物件了,
volatile 的主要是用在于解決多個CPU上運行的多個執行緒可以并且將快取資料和指令重新排序的問題,
如果它不是 volatile 的,并且CPU A遞增了一個值,則CPU B可能直到一段時間后才能真正看到該遞增的值,這可能會引起問題,
如果它是 volatile 的,則僅確保兩個CPU同時看到相同的資料, 它根本不會阻止他們交錯讀取和寫入操作,而這正是您要避免的問題,
使用 lock 也可以防止上述多CPU重新排序問題,所以使用了 lock 就可以不需要再 volatile 了
很多 Java 的單例模式實作強調要使用 volatile 關鍵詞來防止指令重新排序的問題,但是實際上可能并不需要,王爭在他的設計模式專欄中指出只有很低的 JDK 版本才需要這樣做,我們現在用的高版本的 JDK 已經在內部處理了,不需要再加 volatile
Reference
- https://github.com/WeihanLi/DesignPatterns/tree/master/CreatePattern/SingletonPattern
- https://stackoverflow.com/questions/154551/volatile-vs-interlocked-vs-lock
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/4870.html
標籤:C#
上一篇:log4Net 之 詳細組態檔
下一篇:RabbitMQ安裝
