設計模式(1)單例模式(Singleton)


設計模式(0)簡單工廠模式

源碼地址

0 單例模式簡介

0.0 單例模式定義

單例模式是GOF二十三中經典設計模式的簡單常用的一種設計模式,單例模式的基本結構需滿足以下要求。

  • 單例模式的核心結構只有一個單例類,單例模式要保證這個類在運行期間只能被實例化一次,即只會被創建唯一的一個單例類的實例。
  • 單例模式需要提供一個全局唯一能得到這個類實例的訪問點,一般通過定義一個名稱類似為GetInstance的公用方法實現這一目的。

要滿足上面的兩點要求,應該很容易的想到:

1.該類的構造函數應該是私有的,不能隨意被實例化是保證只有一個實例的前提。

2.該類需提供一個公開的且返回值類型為單例類類型的公用方法。

來看一下單例模式的基本結構圖:

1

0.1 單例模式應用場景

通過上面對單例模式基本定義的了解,單例模式的應用場景也就很明確了。

單例模式適用於各種系統中某個類的對象只能存在一個類似場景, 我們現在回顧一下上一篇簡單工廠模式中的大致實現

/// <summary>
 /// 簡單工廠類
 /// </summary>
 public class Factory
 {

     /// <summary>
     /// 創建英雄的靜態方法
     /// </summary>
     /// <param name="heroName">英雄名稱</param>
     /// <returns></returns>
     public static IHero CreateHero(string heroName)
     {
         switch (heroName)
         {
             case "DH":
                 return new DH();
             case "WD":
                 return new WD();
             case "KOG":
                 return new KOG();
             case "POM":
                 return new POM();
             default:
                 return null;
         }
     }
 }
/// <summary>
 /// 惡魔獵手
 /// </summary>
 public class DH : IHero
 {

     /// <summary>
     /// 秀出自己的技能
     /// </summary>
     public void ShowSkills()
     {
         Console.WriteLine("我是惡魔獵手,我會法力燃燒、獻祭、閃避和變身。");
     }
 }

通過簡單工廠模式確實達到了接口隔離的目的,外部使用無需關注內部類的具體實現工程,只通過簡單工廠類創建想要的對象即可,但這里有一個致命的問題就是,我們玩兒游戲的過程中,英雄會存在一個死亡和復活的場景,我們簡單的把英雄祭壇理解為創建英雄的簡單工廠,假設當我們復活英雄的時候,是通過工廠類創建英雄的一個過程,那么我們面臨的問題就出現了,我本來一個6級的大惡魔獵手,由於走位過度風騷,走進了祭壇,現在在通過工廠創建的時候,由於是又重新new了一個對象,從祭壇中走出了一個萌叉叉的1級小惡魔獵手……

為保證我的那個6級大惡魔還是那個6級大惡魔,一身裝備一個不少的走出祭壇,至此也就到了必須引入單例模式的時候了。

1 單例模式詳解

1.0單例模式的基本實現-懶漢式單例模式

按照單例模式的2個基本特征:私有的構造函數公開的GetInstance方法。將DH類進行如下改造,代碼的具體意圖已經通過注釋詳細解釋。

/// <summary>
/// 惡魔獵手
/// </summary>
public class DH : IHero
{
    //定義一個靜態的DH類變量
    private static DH dh;

    /// <summary>
    /// 私有的構造函數,能夠保證該類不會在外部被隨意實例化,是保證該類只用一個實例的基本前提
    /// </summary>
    private DH()
    {

    }

    /// <summary>
    /// 定義一個靜態的公開的GetInstance方法供外部得到DH類唯一實例是調用
    /// </summary>
    /// <returns></returns>
    public static DH GetInstance()
    {
       //先判斷dh是否已經被實例化,若未被實例化,先實例化得到DH類的實例
        //保證DH類只被實例化一次
        if (dh == null)
        {
            dh = new DH();
        }
        return dh;
    }

    /// <summary>
    /// 秀出自己的技能
    /// </summary>
    public void ShowSkills()
    {
        Console.WriteLine("我是惡魔獵手,我會法力燃燒、獻祭、閃避和變身。");
    }
}

修改Factory簡單工廠類中創建DH實例部分的代碼

/// <summary>
/// 簡單工廠類
/// </summary>
public class Factory
{

    /// <summary>
    /// 創建英雄的靜態方法
    /// </summary>
    /// <param name="heroName">英雄名稱</param>
    /// <returns></returns>
    public static IHero CreateHero(string heroName)
    {
        switch (heroName)
        {
            case "DH":
                return DH.GetInstance(); //通過DH類公開的靜態GetInstance方法得到DH類的實例
            case "WD":
                return new WD();
            case "KOG":
                return new KOG();
            case "POM":
                return new POM();
            default:
                return null;
        }
    }
}

客戶端測試

static void Main(string[] args)
{
    IHero dh1 = Factory.CreateHero("DH");
    IHero dh2 = Factory.CreateHero("DH");
    if (dh1.Equals(dh2))
        Console.WriteLine("惡魔獵手:我還是從前的我。");
    else
        Console.WriteLine("惡魔獵手:我已不是從前的我。");

    IHero wd1 = Factory.CreateHero("WD");
    IHero wd2 = Factory.CreateHero("WD");
    if (wd1.Equals(wd1))
        Console.WriteLine("守望者:我還是從前的我。");
    else
        Console.WriteLine("守望者:我已不是從前的我。");

    Console.ReadLine();
}

輸出結果如下

1

至此我們對DH這個類應用了單例模式來確保無論何時走出祭壇的都是同一個DH對象,從DH對象被實例化的實際來看,是在被使用的時候才會被創建,這種方式被成為懶漢式單例模式

有一天突發奇想,我建造兩個英雄祭壇(兩個簡單工廠類),用我APM500+的超快手速,同時在兩個祭壇里生產同一個英雄,發現我擁有了2個6級大惡魔……(當然了,實際中不會有這個bug存在)

這就是基本懶漢式單例模式要面對的多線程問題,也就是說基本懶漢式單例模式的寫法是無法做到線程級別安全的

問題的關鍵就在獲取DH類實例的GetInstance方法的內部實現中

if (dh == null)
{
   dh = new DH();
}
return dh;

簡單來說就是當第一個線程調用判斷if(dh==null)為true,已經進入內部通過調用new進行實例化時,另一個線程也進行了判斷,而恰恰此時dh還沒有被實例化完成,同樣第二個線程也進入if判斷語句的內部,進行dh的實例化,於是就出現了2個DH類的實例,從兩個祭壇走出來兩個大惡魔。

解決這一問題一般有兩種方法餓漢式單例雙重檢查鎖。

1.1 餓漢式單例

餓漢式單例是在系統初始化時自動完成單例類實例的一種方法,而不是等到需要的時候再初始化,也就是說不管以后你會不會用到這個類的對象,我都會給你實例化一個出來,有一種飢餓難耐的感覺在里面,故名餓漢式。

/// <summary>
/// 餓漢式單例
/// </summary>
public class DH : IHero
{
    //系統初始化時已經將DH類進行實例化
    private static readonly DH dh = new DH();

    /// <summary>
    /// 私有的構造函數,能夠保證該類不會在外部被隨意實例化,是保證該類只用一個實例的基本前提
    /// </summary>
    private DH()
    {

    }

    /// <summary>
    /// 調用時直接返回已經實例化完成的對象
    /// </summary>
    /// <returns></returns>
    public static DH GetInstance()
    {
        return dh;
    }

    /// <summary>
    /// 秀出自己的技能
    /// </summary>
    public void ShowSkills()
    {
        Console.WriteLine("我是惡魔獵手,我會法力燃燒、獻祭、閃避和變身。");
    }
}

這種方法簡單直接的解決了線程安全問題,但是由於實在初始化時就將單例類進行了實例化,一定程度上造成了各種資源的浪費,違背了延遲加載的設計思想,一般為了解決單例模式線程安全問題,通常使用雙重檢查鎖的方法。

1.2 雙重檢查鎖

雙重檢查鎖的命名基於單重檢查鎖方式而來,單重檢查鎖是在GetInstance實現的時候先行進行鎖定,防止別的線程進入,從而解決線程安全問題的。主要代碼如下

//定義一個靜態只讀的用於加鎖的輔助對象
private static readonly object lockObject = new object ();
lock (lockObject)
{
    //先判斷dh是否已經被實例化,若未被實例化,先實例化得到DH類的實例
    //保證DH類只被實例化一次
    if (dh == null)
    {
        dh = new DH();
    }
}
return dh;

這種方式每次都要進行lock操作,實際上是一種同步方式,這將會在一定程度上影響系統性能的瓶頸和增加了額外的開銷。由此衍生出了雙重檢查鎖的方式,簡單來說就是先判斷一次dh是否為null,為null時才進行lock操作,不為null就直接返回。

/// <summary>
/// 惡魔獵手
/// </summary>
public class DH : IHero
{
    //定義一個靜態的DH類變量
    private static DH dh;
    //定義一個靜態只讀的用於加鎖的輔助對象
    private static readonly object lockObject = new object (); 
    /// <summary>
    /// 私有的構造函數,能夠保證該類不會在外部被隨意實例化,是保證該類只用一個實例的基本前提
    /// </summary>
    private DH()
    {

    }

    /// <summary>
    /// 定義一個靜態的公開的GetInstance方法供外部得到DH類唯一實例是調用
    /// </summary>
    /// <returns></returns>
    public static DH GetInstance()
    {
        //先判斷dh是否已經被實例化,若未被實例化,先加鎖保證線程安全
        if (dh == null)
        {
            lock (lockObject)
            {
              //先判斷dh是否已經被實例化,若未被實例化,先實例化得到DH類的實例
                //保證DH類只被實例化一次
                if (dh == null)
                {
                    dh = new DH();
                }
            }
        }
        return dh;
    }

    /// <summary>
    /// 秀出自己的技能
    /// </summary>
    public void ShowSkills()
    {
        Console.WriteLine("我是惡魔獵手,我會法力燃燒、獻祭、閃避和變身。");
    }
}

2 總結

本次主要基於上一篇的簡單工廠模式,延續的學習使用了單例工廠模式確保一個類實例的全局唯一性,過程中學習了懶漢式、餓漢式、雙重檢查鎖等具體解決方案及演變過程。

設計模式從來不是單打獨斗,核心思想是要根據實際需要利用多種模式互相配合來實現代碼結構的最優化和健壯性。


免責聲明!

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



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