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

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

源碼地址git

0 單例模式簡介

0.0 單例模式定義

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

  • 單例模式的核心結構只有一個單例類,單例模式要保證這個類在運行期間只能被實例化一次,即只會被建立惟一的一個單例類的實例。
  • 單例模式須要提供一個全局惟一能獲得這個類實例的訪問點,通常經過定義一個名稱相似爲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 總結

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

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

相關文章
相關標籤/搜索