三言兩語說透設計模式的藝術-單例模式
寫在前面
單例模式是一種常用的軟件設計模式,它所創建的對象只有一個實例,且該實例易于被外界訪問。單例對象由于只有一個實例,所以它可以方便地被系統中的其他對象共享,從而減少系統中的資源開銷。
單例模式
單例模式的實現思路是:
- 構造函數需要被私有化,外部無法直接通過new來創建對象實例。
- 提供一個靜態的公有訪問點,用于獲取單例對象的實例。
- 通過判斷實例是否已經存在來決定創建或直接返回現有實例。
單例模式的要點:
- 某個類只能有一個實例
- 它必須自行創建實例
- 它必須自行向整個系統提供整個實例
我們來看一下使用TypeScript實現單例模式的代碼示例:
class Singleton {
// 私有靜態屬性,存儲唯一實例
private static instance: Singleton;
// 私有構造函數,防止外部實例化
private constructor() {}
// 向外部提供能夠共享訪問的唯一實例
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
// 其他方法和屬性
}
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true上面代碼中,Singleton類的構造函數被private修飾,使其無法在類的外部通過new來創建實例。
getInstance方法首先會判斷實例是否存在,如果不存在才去新建實例,如果實例已存在則直接返回現有實例。這確保了整個程序中只會創建該類的一個實例。
測試代碼中,s1和s2實際上是獲取的是同一個實例對象。
圖片
單例模式的優點:
- 對唯一實例的受控訪問。
- 由于單例對象存放在靜態變量中,所以可以直接通過類名訪問,簡單方便。
- 可以避免對資源的重復占用。
單例模式的缺點:
- 沒有抽象層,擴展困難。
- 單例類的職責過重,違反單一職責原則。
- 沒有接口,依賴具體實現,導致擴展性差。
Singleton單例:在單例類的內部實現只生成一個實例,同時提供一個靜態方法getInstance()方法,讓用戶可以訪問它的唯一實例;為了防止在外部對單例類實例化,它的構造函數可見性為private;在單例類內部定義了一個Singleton類型的靜態屬性instance,作為提供給外部共享訪問的唯一實例。
餓漢式單例類
餓漢式單例類:當類被加載時,靜態屬性instance會被初始化,此時類的私有構造函數會被調用,單例類的唯一實例將會被創建。
普通單例模式和餓漢式單例模式的區別:
- 普通單例模式是在第一次調用getInstance方法時才創建實例對象。
- 餓漢式是無論是否調用都會在類加載時就創建實例對象。
下面我們使用TypeScript代碼實現一個餓漢式單例:
class Singleton {
private static instance = new Singleton();
private constructor() {}
public static getInstance() {
return Singleton.instance;
}
}
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true餓漢式單例由于在類加載時就完成了初始化,所以理論上它是線程安全的,在多線程環境下也能保證單例。
但餓漢式也有可能造成不必要的實例化,如果這個單例的實例對象較大,而客戶端又沒調用getInstance方法,那就會浪費內存。
懶漢式單例模式
其實懶漢式單例模式,就是前面提到的普通單例模式。
懶漢式單例模式實現代碼如下:
class Singleton {
private static instance: Singleton;
private constructor() {}
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}但是,這種實現方式存在一個問題,就是在多線程環境下會存在安全隱患。
如果有兩個線程A和B,它們同時調用 getInstance 方法,并且實例還沒有被初始化,那么它們會同時執行 Singleton.instance = new Singleton();這行代碼。
這樣就會導致實際創建了兩個實例,違反了單例模式的初衷。
為了使懶漢式單例在多線程中也是安全的,我們可以對getInstance方法加鎖:
class Singleton {
private static instance: Singleton;
private constructor() {}
public static getInstance(): Singleton {
if (!Singleton.instance) {
// 加鎖
lock()
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
// 釋放鎖
unlock()
}
return Singleton.instance;
}
}這樣當一個線程進入該方法時,其它線程就只能等待,直到鎖被釋放后才能進入方法。
這就確保了單例實例的唯一性。這里的鎖機制可以使用互斥量mutex等各種鎖的實現。
以上是關于懶漢式單例線程安全性問題的一個補充說明。讓我們的單例模式實現更加健壯。
餓漢式單例與懶漢式單例類比較
- 實例化時機不同
- 懶漢式是在第一次調用getInstance時才實例化Singleton對象
- 餓漢式是在類加載時就實例化了Singleton對象
- 資源利用效率不同
- 懶漢式更節約資源,按需實例化,如果一直沒有調用getInstance也不會實例化
- 餓漢式不管是否需要都會實例化,如果長時間沒有使用就會浪費內存
- 多線程安全性不同
- 餓漢式天然是多線程安全的,因為實例在類加載時就已經創建好了
- 懶漢式需要額外的同步機制來保證多線程安全
- 使用場景不同
- 懶漢式更適合實例化過程比較耗時或耗資源的情況
- 餓漢式更適合實例化過程很快且確定會用到的情況
懶漢式相比餓漢式更加靈活,但需要處理多線程安全問題。餓漢式編寫簡單但不太高效。
在實際開發中,我們可以根據需求選擇合適的實現方式,也可以采用雙重校驗鎖等線程安全的懶漢式實現。
一種更好的單例實現方式
餓漢式單例類不能實現延遲加載,不管將來用不用,它始終占據內存;懶漢式單例類線程安全控制繁瑣,而且性能收到影響。對此,無論是餓漢式單例還是懶漢式單例都在一些問題,使用IoDH(Initialization on Demand Holder)可以結合兩者的優點,克服兩者的缺點實現性能和實現更優的單例模式。
IoDH是一種技術方案,它利用了類的靜態屬性來實現延遲加載和線程安全。要實現IoDH,只需在但李磊中增加靜態內部類即可,在該內部類中創建單例對象,再將該單例對象通過getInstance()方法返回給外部使用。
// 單例服務接口
interface SingletonService {
doSomething(): void;
}
// 單例服務類
class SingletonServiceImpl implements SingletonService {
doSomething() {
console.log('Doing something...');
}
}
// IoC容器類
class IoCContainer {
private singleton: SingletonService;
constructor() {
this.singleton = new SingletonServiceImpl();
}
getSingleton(): SingletonService {
return this.singleton;
}
}
// 測試代碼
const container = new IoCContainer();
const s1 = container.getSingleton();
const s2 = container.getSingleton();
console.log(s1 === s2); // true詳細解析一下使用IoC容器實現單例模式的代碼:
- 定義了單例服務接口SingletonService,用于規范單例對象的操作。
- SingletonServiceImpl實現了該接口,作為單例對象的具體實現類。
- IoC容器類IoCContainer在內部持有SingletonService類型的成員變量singleton。
- IoC容器類的構造函數中會實例化這個singleton對象,確保全局只有這一個實例。
- getSingleton()方法用來返回這個singleton實例。
- 在測試代碼中,從IoC容器中獲取了兩次單例對象,并比較它們的引用是否相同。
- 運行結果證明兩次獲取的確是同一個對象引用,即單例。
這樣通過IoC容器管理單例的創建,可以實現:
- 把單例對象的創建和生命周期管理轉移到IoC容器。
- 外部代碼不需要關心單例內部的具體實現,只需要從容器中獲取實例即可。
- 符合單一職責原則,程序邏輯更清晰。
- 有利于代碼的可測試性,可以通過mock容器進行單元測試。
- 擴展性較好,如果要切換不同的單例實現,只需要調整容器中的對象創建即可。
總結
單例模式作為一種設計模式,由于具有明確的目的、簡單的結構和易于理解的特點,在軟件開發中使用頻率很高,在許多應用程序和框架中都有廣泛應用。
- 單例模式的主要優點包括:提供對唯一實例的受控訪問,由于全局只存在一個實例,因此可以節約系統資源;允許擴展為可變數量的實例,既節約資源又解決過度共享影響性能的問題。
- 單例模式的主要缺點包括:沒有抽象層導致擴展性差;違反單一職責原則,將實例化和業務邏輯混合在一起;在支持垃圾回收的運行時環境下可能導致狀態丟失。
- 使用單例模式的典型場景包括:系統只需要一個實例;客戶只能通過一個公共訪問點獲取實例;需要節約資源的頻繁創建銷毀對象。
總之,單例模式是一種利用率較高的設計模式,其限制實例個數的特點可以帶來節省資源的優勢,但也可能導致擴展性較弱以及與語言環境不夠匹配等問題。在軟件設計中,開發者需要權衡考慮系統的需求和優缺點,適當使用單例模式。































