設計模式之單例模式


設計模式之單例模式

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM