程式設計師羽化之路--我眼中的單例模式並不完美

架構師修行之路發表於2020-07-08

/// <summary>
    /// 全域性唯一的配置資訊
    /// </summary>
   public class Config
    {
        private static Config _config = null;
        public static Config GetConfig()
        {
            if (_config == null)
            {
                _config = new Config();
            }
            return _config;
        }
    }

單例模式

所謂單例,就是整個程式有且僅有一個例項。該類負責建立自己的物件,同時確保只有一個物件被建立。

幾乎大部分程式設計師面試的時候,面試官讓你說出三種常用的設計模式,單例必是其中之一。平時所說的單例模式是指一個程式內只存在某個型別的一個例項,其實擴充套件到叢集這個概念,位於不同物理環境的多個程式之間也可以有單例這種概念,像平時吹水用的分散式鎖其實可以看做是多程式之間的單例模式。

透過單例的現象可以看到單例模式本質上也是解決資源競爭的問題,它讓多個執行緒甚至多個程式共享同一個資源,以達到資源共享的目的。為什麼要實現資源共享呢?因為這個資源在業務場景下只能存在一個例項,例如以上的全域性配置資訊,如果程式內部有多個配置資訊例項,不僅浪費了伺服器的記憶體資源,在配置資訊發生修改的時候,多個例項隨之同步更新又是一個很大的問題。

單例實現

單例模式的概念很簡單,實現的方式也有很多,但是關注點卻無外乎以下幾個:

  1. private的建構函式,主要是為了避免外部通過New建立例項
  2. 單例是否支援延遲載入,以及效能是否高效
  3. 多執行緒環境下,物件建立是否安全
  4. 全域性只有一個訪問入口

為了達到以上幾個要求,我們可以有很多的實現方法

餓漢式
public class Config
    {
        private Config() { }
        private static Config _config = new Config ();
        public static Config GetConfig()
        {
           
            return _config;
        }
    }

這種方式主要是利用了語言特性,一個型別的靜態屬性是屬於型別所有,在類的生命週期內只會載入一次,所以以上程式碼實現單例模式並沒有問題,而且超級簡單。

很多人說這種方式不妥,在型別的載入時候就完成例項建立,沒有達到惰性載入,會造成記憶體的浪費。至於這個問題我並不表示完全贊同。如果一個單例的初始化耗時比較長,最好不要等到真正用它的時候才去執行初始化,這樣會影響系統的效能。餓漢式可以實現在程式啟動的時候就進行初始化操作,這樣就能避免初始化時間過長導致的效能問題,而且還有一個比較重要的好處,如果初始化程式有錯誤,我們可以在程式啟動的時候就發現,而不用等到程式上線執行時才暴露出來。這就好比編譯期錯誤永遠比執行時錯誤好排查的道理類似。

懶漢式

程式設計師妹子貢獻的程式碼其實就屬於懶漢式,表面上看可以實現惰性載入,但是在多執行緒的環境下,會產生多個例項,問題就在於 if (_config == null) 這個語句並非是執行緒安全的。如果非要改造的話,可以加上全域性的鎖機制,有一個注意點,這裡鎖的物件一定要是一個static全域性的物件

 private static object objLock = new object();
        private static Config _config = null;
        public static Config GetConfig()
        {
            lock (objLock)
            {
                if (_config == null)
                {
                    _config = new Config();
                }
            }           
            return _config;
        }
雙重加鎖機制

雖然懶漢式方式能保證執行緒安全,但是鎖的機制缺大大降低了系統效能,原因是鎖機制把所有請求順序化了,為了改善懶漢式的效能,所以雙重加鎖機制出現了,在保證了執行緒安全的情況下,大大提高了程式效能

private static object objLock = new object();
        private static Config _config = null;
        public static Config GetConfig()
        {
            if (_config == null)
            {
                lock (objLock)
                {
                    if (_config == null)
                    {
                        _config = new Config();
                    }
                }
            }
                
            return _config;
        }

以上只是實現單例的幾種常用方式,根據每個語言的特性還有很多可以實現單例的方式,比如:利用c#或者java的內部類,靜態初始化特性等都可以實現執行緒安全的單例模式。


單例模式缺陷

  1. 物件導向設計講究封裝,繼承,多型,以及抽象。單例模式對於其中的繼承,多型支援得不好,抽象講究的是面向介面程式設計,單例模式並沒有介面概念。拿以上配置檔案的單例為例,假設現在的配置資訊是以本地檔案的方式進行載入,如果後期要加入從資料庫載入配置資訊這個需求,單例模式必須修改現有程式碼,這在一定程度上就違反了設計規則。所以單例在一定程度上丟失了應對未來需求的擴張性。
  2. 單例模式在職責上有時候會過重,即要負責初始化的過程,又要負責初始化的內容,甚至在某些情況下還要負責其他程式,這在一定程度上違反了“單一職責”原則。
  3. 由於單例模式對外之後一個入口點,並沒有顯示的利用建構函式傳參的方式進行初始化,內部使用了哪些型別並不能很快識別出來,開發人員很難識別出類的依賴關係
  4. 單例模式並不適合那些表面是單例,但是未來還有可能擴充套件的場景。舉個例子:執行緒池在很多程式中都被設計成單例模式,很多開發人員認為程式中只存在一個執行緒池,但是在個別需求下,同一個程式需要多個執行緒池的場景是存在的。

寫在最後

單例模式最為常用的一種模式,有其自己的優勢和適用場景。如果一個型別在程式中要求例項化的數量有要求的,該怎麼辦呢?比如,一個型別可以最多例項化10個,或者每個執行緒可以例項化一個,你可能需要研究一下threadLocal 或者hashmap等知識了。至於叢集間的單例實現歡迎大家在留言區體現!!

相關文章