/// <summary>
/// 全域性唯一的配置資訊
/// </summary>
public class Config
{
private static Config _config = null;
public static Config GetConfig()
{
if (_config == null)
{
_config = new Config();
}
return _config;
}
}
單例模式
所謂單例,就是整個程式有且僅有一個例項。該類負責建立自己的物件,同時確保只有一個物件被建立。
幾乎大部分程式設計師面試的時候,面試官讓你說出三種常用的設計模式,單例必是其中之一。平時所說的單例模式是指一個程式內只存在某個型別的一個例項,其實擴充套件到叢集這個概念,位於不同物理環境的多個程式之間也可以有單例這種概念,像平時吹水用的分散式鎖其實可以看做是多程式之間的單例模式。
透過單例的現象可以看到單例模式本質上也是解決資源競爭的問題,它讓多個執行緒甚至多個程式共享同一個資源,以達到資源共享的目的。為什麼要實現資源共享呢?因為這個資源在業務場景下只能存在一個例項,例如以上的全域性配置資訊,如果程式內部有多個配置資訊例項,不僅浪費了伺服器的記憶體資源,在配置資訊發生修改的時候,多個例項隨之同步更新又是一個很大的問題。
單例實現
單例模式的概念很簡單,實現的方式也有很多,但是關注點卻無外乎以下幾個:
- private的建構函式,主要是為了避免外部通過New建立例項
- 單例是否支援延遲載入,以及效能是否高效
- 多執行緒環境下,物件建立是否安全
- 全域性只有一個訪問入口
為了達到以上幾個要求,我們可以有很多的實現方法
餓漢式
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的內部類,靜態初始化特性等都可以實現執行緒安全的單例模式。
單例模式缺陷
- 物件導向設計講究封裝,繼承,多型,以及抽象。單例模式對於其中的繼承,多型支援得不好,抽象講究的是面向介面程式設計,單例模式並沒有介面概念。拿以上配置檔案的單例為例,假設現在的配置資訊是以本地檔案的方式進行載入,如果後期要加入從資料庫載入配置資訊這個需求,單例模式必須修改現有程式碼,這在一定程度上就違反了設計規則。所以單例在一定程度上丟失了應對未來需求的擴張性。
- 單例模式在職責上有時候會過重,即要負責初始化的過程,又要負責初始化的內容,甚至在某些情況下還要負責其他程式,這在一定程度上違反了“單一職責”原則。
- 由於單例模式對外之後一個入口點,並沒有顯示的利用建構函式傳參的方式進行初始化,內部使用了哪些型別並不能很快識別出來,開發人員很難識別出類的依賴關係
- 單例模式並不適合那些表面是單例,但是未來還有可能擴充套件的場景。舉個例子:執行緒池在很多程式中都被設計成單例模式,很多開發人員認為程式中只存在一個執行緒池,但是在個別需求下,同一個程式需要多個執行緒池的場景是存在的。
寫在最後
單例模式最為常用的一種模式,有其自己的優勢和適用場景。如果一個型別在程式中要求例項化的數量有要求的,該怎麼辦呢?比如,一個型別可以最多例項化10個,或者每個執行緒可以例項化一個,你可能需要研究一下threadLocal 或者hashmap等知識了。至於叢集間的單例實現歡迎大家在留言區體現!!