天天看點

設計模式之單例模式

單例模式

設計模式之單例模式

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

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。