程序員羽化之路–我眼中的單例模式並不完美
/// <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等知識了。至於集群間的單例實現歡迎大家在留言區體現!!