C#中的Singleton模式
阿新 • • 發佈:2020-06-05
[TOC](c#中的Singleton模式)
# 前言
Singleton是二十三個設計模式中比較重要也比較經常使用的模式。但是這個模式雖然簡單,實現起來也會有一些小坑,讓我們一起來看看吧!
# 實現思路
首先我們看看這個設計模式的UML類圖。
![](https://img2020.cnblogs.com/blog/699616/202006/699616-20200605083551766-381491365.png)
很清晰的可以看到,有三點是需要我們在實現這個模式的時候注意的地方。
- 私有化的構造器
- 全域性唯一的靜態例項
- 能夠返回全域性唯一靜態例項的靜態方法
其中,私有化構造器是防止外部使用者建立新的例項而靜態方法用於返回全域性唯一的靜態例項供使用者使用。原理清楚了,接下來我們看看一些典型的實現方式和其中的暗坑。
# 實現方法
## 最簡單的實現方法
最簡單的實現方法自然就是按照UML類圖直接寫一個類,我們看看程式碼。
```csharp
class Program
{
static void Main(string[] args)
{
var single1 = Singleton.Instance;
var single2 = Singleton.Instance;
Console.WriteLine(object.ReferenceEquals(single1, single2));
Console.ReadLine();
}
}
class Singleton
{
private static Singleton _Instance = null;
private Singleton()
{
Console.WriteLine("Created");
}
public static Singleton Instance
{
get
{
if (_Instance == null)
{
_Instance = new Singleton();
}
return _Instance;
}
}
public void DumbMethod()
{
}
}
```
這段程式碼忠實的實現了UML類圖裡面的一切,檢視輸出結果,
![](https://img2020.cnblogs.com/blog/699616/202006/699616-20200605083655032-360163328.png)
證實了Singleton確實起了作用,多次呼叫僅僅產生了一個例項,似乎這麼寫就可以實現這個模式了。但是,真的會那麼簡單嗎?
## 如果多執行緒亂入?
現在我們給剛剛的例子加點調料,假設多個對例項的呼叫,並不是簡單的,彬彬有禮的順序關係,二是以多執行緒的方式呼叫,那麼剛剛那種實現方法,還能從容應對嗎?讓我們試試。把**Main**函式裡面的呼叫改成這樣。
```csharp
static void Main(string[] args)
{
int TOTAL = 10000;
Task[] tasks = new Task[TOTAL];
for (int i = 0; i < TOTAL; i++)
{
tasks[i] = Task.Factory.StartNew(() =>
{
Singleton.Instance.DumbMethod();
});
}
Task.WaitAll(tasks);
Console.ReadLine();
}
```
通過Factory創造出1萬個Task,幾乎同時去請求這個單例,看看輸出。
![](https://img2020.cnblogs.com/blog/699616/202006/699616-20200605083739568-1051667012.png)
咦,我們剛剛寫的Singleton模式失效了,這個類被創造了5次(這段程式碼執行多次,這個數字不一定相同),一定是多執行緒搞的鬼,我們剛剛寫的程式碼沒有辦法應對多執行緒,換句話說,是非執行緒安全的(thread-safe),那有沒有辦法來攻克這個難關呢?
## 執行緒安全的單例模式
### Lock版本
提到執行緒安全,很多同學第一反應就是用**lock**,不錯,lock是個可行的辦法,讓我們試試。新增一個引用型別的物件作為lock物件,修改程式碼如下(什麼?你問我為什必須是引用型別的物件而不能是值型別的物件?因為lock的時候,如果物件是值型別,那麼lock僅僅鎖住了它的一個副本,另外一個執行緒可以暢通無阻的再次lock,這樣lock就失去了阻塞執行緒的意義)
```csharp
private static object _SyncObj = new object();
public static Singleton Instance
{
get
{
lock (_SyncObj)
{
if (_Instance == null)
{
_Instance = new Singleton();
}
return _Instance;
}
}
}
```
執行一下,輸出
![](https://img2020.cnblogs.com/blog/699616/202006/699616-20200605083757095-51529392.png)
只有一個例項建立,證明Lock起作用了,這個模式可行!不過有些不喜歡用Lock的同學可能要問,還有沒有其他辦法呢?答案是有的。
### 靜態構造器版本
回想一下,C#中的類靜態構造器,只會在這個類第一次被使用的時候呼叫一次,天然的執行緒安全,那我們試試不用Lock使用類靜態構造器?修改Singleton類如下:
```csharp
class Singleton
{
private static Singleton _Instance = null;
private Singleton()
{
Console.WriteLine("Created");
}
static Singleton()
{
_Instance = new Singleton();
}
//private static object _SyncObj = new object();
public static Singleton Instance
{
get { return _Instance; }
}
public void DumbMethod()
{
}
}
```
去掉了Lock,添加了一個類靜態構造器,試一試。
![](https://img2020.cnblogs.com/blog/699616/202006/699616-20200605083830318-1792528403.png)
完美!對於不喜歡用Lock(在這個例子中,例項只會建立一次但是之後的所有執行緒都要先排隊Lock再進入Critical code進行檢查,效率比較低下)的同學,類靜態構造器提供了一種很好的選擇。
~~不過俗話說,人心苦不足 ,~~ 我們總是追求卓越。這個版本比Lock版本似乎更好一點,那還有沒有更好的版本呢?有的。
### Lazy版本
從net 4.0開始,C#開始支援延遲初始化,通過**Lazy**關鍵字,我們可以宣告某個物件為僅僅當第一次使用的時候,再初始化,如果一直沒有呼叫,那就不初始化,省去了一部分不必要的開銷,提升了效率。如果你不熟悉Lazy或者想更多瞭解它,請[參考](https://docs.microsoft.com/en-us/dotnet/api/system.lazy-1?view=netframework-4.7.2)。我們今天關注的重點在於,Lazy也是天生執行緒安全的,所以我們嘗試用它來實現Singleton模式?修改程式碼如下:
```csharp
class Singleton
{
private static Lazy