前言
單例模式是我們最常用的設計模式,面試的時候如果問任何一個開發者設計模式,單例模式估計是脫口而出吧,23中常見的設計模式之中並不是所有設計模式都是很常用的,而單例模式絕對是最常用的那一個。但如果真正面試深入問到單例模式,那你確定你真的瞭解嘛?常見的面試會讓你現場寫個單例模式,如果深入一點的問的話會問單例模式有幾種實現方式?用代碼實現並說出各個方式的優缺點?想必如果面試官真這麼問的話,估計絕大多數人也hold不住吧,今天我就來深入的整理一下單例模式。
爲什麼要使用單例模式?
單例模式理解起來很簡單。一個類只允許創建一個對象(或者實例),那這個類就是一個單例類,這種設計模式就叫做單例設計模式,簡稱單例模式。
處理資源訪問衝突
我們先來看一個例子,我們定義一個往文件中打印日誌的Logger類
public class Logger {
private FileWriter writer;
public Logger() {
File file = new File("/Users/dxw/log.txt");
writer = new FileWriter(file, true); //true表示追加寫入
}
public void log(String message) {
writer.write(mesasge);
}
}
// Logger類的應用示例:
public class UserController {
private Logger logger = new Logger();
public void login(String username, String password) {
// ...省略業務邏輯代碼...
logger.log(username + " logined!");
}
}
public class OrderController {
private Logger logger = new Logger();
public void create(OrderVo order) {
// ...省略業務邏輯代碼...
logger.log("Created an order: " + order.toString());
}
}
上面代碼會看到,我們創建了兩個Logger對象,但寫到同一個txt文件中,就有可能存在日誌相互覆蓋的情況,因爲這裏是存在資源競爭的關係,如果我們有兩個線程同時個一個共享變量修改,往裏面寫數據,就有可能相互覆蓋了。有人會想到解決這個問題就是加一個鎖,但如果加對象鎖並不能解決多個對象競爭同一個資源的問題,我們需要加類鎖才行,這個時候我們會想到如果將Logger設計成單例就不會存在這樣的問題了。
表示全局唯一類
在業務概念上,如果有一些數據在系統中只應該保存一份,那就比較適合用單例類。比如常見的配置信息。在系統中,我們只有一個配置文件,當配置文件被加載到內存後,以對象的形式存在,也理所應當只有一份,常見的就是遊戲數據配表。再比如,唯一ID號碼生成器,如果程序中有兩個對象就會存在重複ID的情況,所以,我們應該將ID生成器設計爲單例。
單例設計模式常見寫法
- 餓漢式(靜態常量)
//餓漢式(靜態變量)
public class Singleton1
{
//構造器私有化,外部不能new,不寫這個構造函數則會默認有一個公有的構造函數,外部就可以new,這樣不符合單例模式
private Singleton1()
{
}
//在內部創建一個實例對象
private static Singleton1 instance = new Singleton1();
//提供一個公有的靜態方法,返回實例對象
public static Singleton1 GetInstance()
{
return instance;
}
}
使用測試
Singleton1 instance1 = Singleton1.GetInstance();
Singleton1 instance2 = Singleton1.GetInstance();
Debug.Log(string.Format("instance1和instance2是否相等:{0}", instance1 == instance2));
Debug.Log("instance1的hashCode:" + instance1.GetHashCode() + " instance2的hashCode:" + instance2.GetHashCode());
測試效果
優缺點說明:
優點:
寫法簡單,在類裝在的時候就實現了實例化,避免了線程同步的問題。
缺點:
在類裝在的時候就完成實例化,沒有達到Lazy Loading的效果。如果從開始至終都沒有使用過這個實例,則會造成內存的浪費。
結論:
這種方式單例模式可用,可能造成內存的浪費。
- 餓漢式2
實例化的操作也可以放在私有構造函數內
public class Singleton2
{
private static Singleton2 instance;
//將實例化放在私有構造函數裏面
private Singleton2()
{
instance = new Singleton2();
}
//提供一個公有的靜態方法,返回實例對象
public static Singleton2 GetInstance()
{
return instance;
}
}
- 懶漢式
public class Singleton3
{
private static Singleton3 instance;
private Singleton3() { }
//提供靜態公有方法返回實例對象
public static Singleton3 GetInstance()
{
//需要用到的時候再實例化單例對象,即懶漢式
if(instance == null)
{
instance = new Singleton3();
}
return instance;
}
}
優缺點說明:
1.起到了懶加載的效果,但只能在單線程下使用。
2.如果在多線程下,一個線程還沒進入if(instance == null)的邏輯,另外一個線程也進行了訪問,又進行了對象的創建就會產生多個實例。
結論:
在實際開發中,不要用這種方式,但Unity遊戲開發一般不使用多線程的方式,所以Unity遊戲開發中這種模式還是用的挺多的。
- 懶漢式(線程安全,同步方法)
public class Singleton5
{
private Singleton5(){ }
private static readonly object syncObj = new object();
private static Singleton5 instance = null;
public static Singleton5 Instance
{
get
{
lock (syncObj) //添加同步鎖
{
if (instance == null)
instance = new Singleton5();
}
return instance;
}
}
}
優缺點說明:
1.解決了線程不安全的問題
2.效率太低,每個線程想訪問的時候都需要執行一次同步,如果一個線程加鎖,第二個線程只能等待,效率太低。
結論:
實際開發中,不推薦使用這種方式。
- 懶漢式(雙重檢測)
public class Singleton6
{
private Singleton6() { }
private static readonly object syncObj = new object();
private static Singleton6 instance = null;
public static Singleton6 Instance
{
get
{
//雙重檢測提高效率
if (instance == null)
{
lock (syncObj) //添加同步鎖
{
if (instance == null)
instance = new Singleton6();
}
}
return instance;
}
}
}
優點說明:
既解決了懶加載的問題,又解決了線程同步的效率問題。
總結:
在實際開發中推薦使用的方式。
Unity中常見的通用單例模式
Unity開發中最常創建單例模式的就是各種Manager,在程序啓動的時候首先實例化各種單例的Manager,而我見過一個主程是這樣寫的,每一個Manager自己內部定義一個static xxx instance,然後在GetInstance方法中實例化這個instance返回,就會顯得很重複,一般Unity中是這樣創建通用泛型單例類的
創建非MonoBehavior單例
public class Singleton<T> where T : new()
{
protected static T _instance;
public static T sInstance
{
get
{
if (_instance == null)
_instance = new T();
return _instance;
}
}
}
我們使用的時候
public class BattleManager : Singleton<BattleManager>
{
}
因爲Unity裏面幾乎不太使用多線程,所以這裏就沒考慮加鎖的情況。
創建MonoBehavior單例
public class MonoSingleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
public static T sInstance
{
get { return _instance; }
}
protected void Awake()
{
if(_instance != null)
{
DestroyImmediate(gameObject);
return;
}
_instance = gameObject.GetComponent<T>();
InitOnAwake();
}
protected void OnDestroy () {
if(_instance == this)
{
ReleaseOnDestroy();
_instance = null;
}
}
protected virtual void InitOnAwake() {}
protected virtual void ReleaseOnDestroy() {}
}
使用
public class BattleCameraConfig : MonoSingleton<BattleCameraConfig>
{
}
顧名思義就是MonoBehavior的單例是可以掛在GameObject上的。
單例模式的弊端以及替代方案
1.單例模式存在的問題?
- 單例對OOP特性的支持不友好
OOP的四大特性是封裝、抽象、繼承、多態。單例這種設計模式對於其中抽象、繼承、多臺都支持的不好,舉例說明:
public class Order {
public void create(...) {
//...
long id = IdGenerator.getInstance().getId();
//...
}
}
public class User {
public void create(...) {
// ...
long id = IdGenerator.getInstance().getId();
//...
}
}
IdGenerator的使用方式違背了基於接口而非實現的設計原則,也就違背了廣義上理解的OOP的抽象特性。如果未來某一天,我們希望針對不同的業務採用不同的ID生成算法。比如,訂單ID和用戶ID採用不同的ID生成器生成。爲了應對這個需求的變化,我們需要修改到所有用到IdGenerator類的地方,這樣改動就會比較大。
public class Order {
public void create(...) {
//...
long id = IdGenerator.getInstance().getId();
// 需要將上面一行代碼,替換爲下面一行代碼
long id = OrderIdGenerator.getIntance().getId();
//...
}
}
public class User {
public void create(...) {
// ...
long id = IdGenerator.getInstance().getId();
// 需要將上面一行代碼,替換爲下面一行代碼
long id = UserIdGenerator.getIntance().getId();
}
}
除此之外,單例對繼承、多態特性的支持也不友好。這裏"不友好"並不是"完全不支持",從理論上講,單例類也可以被繼承、也可以實現多態,只是實現起來會非常奇怪,導致代碼可讀性變差。不明白設計意圖的人,看到這樣的設計,會覺得莫名其妙。所以,一旦選擇將某個類設計成單例類,也就意味着放棄了繼承和多態這兩個強有力的面向對象特性,也就是相當於損失了可以應對未來需求變化的擴展性。
-
單例會隱藏類與類之間的依賴關係
我們知道代碼的可讀性非常重要,在閱讀代碼的時候,我們希望一眼就能看出類與類之間的依賴關係,搞清楚這個類依賴了哪些外部類。通過構造函數、參數傳遞等方式聲明的類之間的依賴關係,我們通過查看函數的定義,就能很容易識別出來。但是,單例類不需要顯示創建、不需要依賴參數傳遞,在函數中直接調用就可以了。如果代碼比較複雜,這種調用關係就會非常隱蔽。所以在閱讀代碼的時候,我們就需要仔細查看每個函數的代碼實現,才知道這個類到底依賴了哪些類。 -
單例對代碼的擴展性不友好
我們知道單例類智能有一個對象實例。如果未來某一天,我們需要在代碼中創建兩個實例或者多個實例,那就要對代碼有比較大的改動。舉個例子,軟件系統設計初期,我們可能會覺得系統中只應該有一個數據庫連接池,這樣能方便我們控制數據庫連接資源的消耗,所以數據庫連接池就被設計成了單例類。但之後發現,我們可能會用到好幾種數據庫,但不同的數據庫的訪問接口都不一樣,這樣我們就需要不同的連接池對象,也就是不能設計成單例類,實際上一些開源的數據庫連接池也確實沒設計成單例類,我一開始剛接觸.NET開發的時候就碰到過MSSQL和Oracle之間的切換。 -
單例對代碼的可測試性不友好
-
單例不支持有參數的構造函數
2.單例有什麼替代方案?
爲了保證全局唯一性,除了使用單例,我們還可以用靜態方法來實現。不過靜態方法這種實現思路,並不能解決上面提到的問題。如果要完全解決這些問題,我們需要從根本上尋找其他方式來實現全局唯一類,可以由程序員自己來保證不要創建兩個類的對象。
設計模式系列教程彙總
http://dingxiaowei.cn/tags/設計模式/