文章目錄
- 1. 基本概念
- 2. 設計要點
- 3. 實現方式
- 4. 詳解懶漢模式
1. 基本概念
線程安全(Thread Safety)
線程安全是指在多線程環境下,某個函數、類或代碼片段能夠被多個線程同時調用時,仍能保證數據的一致性和邏輯的正確性,不會因線程切換導致錯誤結果。
單例模式(Singleton Pattern)
單例設計模式是一種創建型設計模式,其核心目的是確保一個類只有一個實例存在,并提供全局訪問點來獲取該實例。它常用于管理全局資源(如配置信息、日志系統、數據庫連接池等),避免重復創建和資源競爭。
2. 設計要點
- 構造函數和析構函數是私有的,不允許外部生成和釋放
- 禁止外部實例化:外部代碼無法通過
new
或直接聲明的方式創建對象,確保唯一實例的控制權在類自身。 - 控制生命周期:析構函數私有化可防止外部意外刪除單例對象,保證其生命周期與程序一致。
- 符合單一職責原則:類的創建和銷毀邏輯由自身管理,避免外部干擾。
- 禁止外部實例化:外部代碼無法通過
- 靜態成員變量和靜態返回單例的成員函數
- 全局訪問點:通過靜態方法
getInstance()
提供統一的實例獲取方式,替代直接訪問全局變量。 - 延遲初始化(懶漢式):僅在首次調用時創建實例,節省資源。
- 線程安全(需額外處理):可通過鎖或局部靜態變量(C++11 后)確保多線程安全。
- 在單例模式中,如果多個線程同時調用
getInstance()
方法,可能導致多次創建實例(如懶漢模式未加鎖時),破壞單例的唯一性。 - 解決方案:
- 加鎖(互斥量):在
getInstance()
中使用互斥鎖(如std::mutex
)確保線程同步。 - 局部靜態變量(C++11):利用編譯器保證局部靜態變量的初始化是線程安全的。
- 餓漢模式:提前初始化實例,避免多線程競爭。
- 加鎖(互斥量):在
- 在單例模式中,如果多個線程同時調用
- 全局訪問點:通過靜態方法
- 禁用拷貝構造函數和賦值運算符
- 防止拷貝:避免通過拷貝構造函數復制單例對象,破壞唯一性。
- 防止賦值:禁止通過賦值運算符覆蓋單例對象,如
instance2 = instance1
。 - 強制單例約束:從語法層面杜絕意外破壞單例模式的行為。
要點 | 解決的問題 | 實際意義 |
---|---|---|
私有構造/析構 | 外部隨意創建或銷毀實例 | 確保實例的唯一性和可控性 |
靜態成員與訪問方法 | 全局訪問與資源管理 | 提供統一入口,支持延遲初始化與線程安全 |
禁用拷貝與賦值 | 意外復制導致多實例 | 維護單例的嚴格唯一性 |
3. 實現方式
懶漢模式
懶漢模式的核心是延遲初始化(Lazy Initialization),即在首次調用 getInstance()
時才創建單例實例。在此之前,實例未被分配內存。
特點
- 優點:
- 節省資源:若單例對象未被使用,則不會創建。
- 適合初始化耗時的對象(如文件系統、網絡連接)。
- 缺點:
- 需處理線程安全問題(多線程下可能重復創建)。
- 首次訪問可能因初始化導致延遲。
餓漢模式
餓漢模式的核心是提前初始化,即在程序啟動時(或類加載時)直接創建單例實例,無論是否被使用。
特點
- 優點:
- 線程安全:實例在程序啟動時初始化,避免多線程競爭。
- 代碼簡單:無需處理復雜的線程同步邏輯。
- 缺點:
- 可能浪費資源:即使未使用單例對象,也會占用內存。
- 初始化時間可能影響程序啟動速度。
實現樣例:
class Singleton {
public:static Singleton* getInstance() {return &instance; // 直接返回已初始化的實例}
private:static Singleton instance;Singleton() {}~Singleton() {}
};
// 程序啟動時初始化(餓漢模式)
Singleton Singleton::instance;
對比懶漢模式與餓漢模式
特性 | 懶漢模式 | 餓漢模式 |
---|---|---|
初始化時機 | 首次調用 getInstance() 時 | 程序啟動時(或類加載時) |
線程安全 | 需額外處理(如加鎖或 C++11 特性) | 天然線程安全 |
資源占用 | 按需分配,節省資源 | 提前占用內存,可能浪費資源 |
適用場景 | 初始化耗時、使用頻率不確定的對象 | 初始化簡單、使用頻繁的對象 |
實際開發中,推薦使用 C++11 的局部靜態變量懶漢模式(Meyers’ Singleton,線程安全且代碼簡潔),或根據場景選擇餓漢模式。
4. 詳解懶漢模式
參考:【C++面試題】手撕單例模式_嗶哩嗶哩_bilibili
樣例1
class Singleton1 {
public:// 要點2static Singleton1 * GetInstance() {if(_instance == nullptr) {_instance = new Singleton1();}return _instance;}
private:// 要點1Singleton1() {}~Singleton1() {std::cout << "~Singleton1()\n";}// 要點3Singleton1(const Singleton1 &) = delete;Singleton1& operator = (const Singleton1&) = delete;Singleton1(Singleton1 &&) = delete;Singleton1& operator = (Singleton1 &&) = delete;// 要點2static Singleton1 *_instance;
};
Singleton1* Singleton1::_instance = nullptr;
存在錯誤:
- 該類創建的單例對象在堆中,雖然資源會被釋放,但其在釋放的時候是無法調用析構函數的。
- 非線程安全
樣例2
class Singleton2 {
public:static Singleton2 * GetInstance() {if(_instance == nullpte) {_instance = new Singleton2();atexit(Destructor);}return _instance;}
private:static void Destructor() {if(nullptr != _instance) {delete _instance;_instance = nullptr;}}Singleton2() {}~Singleton2() {std::cout << "~Singleton2()\n";}Singleton2(const Singleton2 &) = delete;Singleton2& operator = (const Singleton2&) = delete;Singleton2(Singleton2 &&) = delete;Singleton2& operator = (Singleton2 &&) = delete;static Singleton2 *_instance;
};
Singleton2* Singleton2::_instance = nullptr;
針對樣例1的問題,添加atexit()
,在程序結束時手動釋放對象,從而調用析構函數
存在問題:
- 非線程安全
樣例3:
class Singleton3 {
public:static Singleton3 * GetInstance() {std::lock_guard<std::mutex> lock(_mutex);if(_instance == nullptr) {std::lock_guard<std::mutex> lock(_mutex);if(_instance == nullptr) {_instance = new Singleton3();// 1. 分配內存// 2. 調用構造函數// 3. 返回對象指針 atexit(Destructor);}}return _instance;}
private:static void Destructor() {if(nullptr != _instance) {delete _instance;_instance = nullptr;}}Singleton3() {}~Singleton3() {std::cout << "~Singleton3()\n";}Singleton3(const Singleton3 &) = delete;Singleton3& operator = (const Singleton3&) = delete;Singleton3(Singleton3 &&) = delete;Singleton3& operator = (Singleton3 &&) = delete;static Singleton3 *_instance; static std::mutex _mutex;
};
Singleton3* Singleton3::_instance = nullptr;
std::mutex Singleton3::_mutex;
在創建實例對象是使用互斥鎖來實現線程安全
-
單檢測
先加鎖,再判斷是否需要創建對象;
該方法只需要檢測一次,但是在已經創建對象的情況下,只需要檢測然后返回就行,不需要再第一次檢測前加鎖(力度過大,效率低)
-
雙檢測(Double-Checked Locking,DCL)
先做第一次檢測,然后在需要創建對象時才加鎖,此時多線程程序會出現多個線程同時通過一次檢測到創建對象的代碼塊,所以需要第二次檢測對象是否創建來避免重復創建
存在問題:
在多線程程序中,CPU會進行指令重排,如new
操作的正常順序應該是(1-2-3),在指令重排之后執行順會變為(1-3-2)。此時如果某個線程執行到new
的“返回對象指針操作”,而另外一個線程執行到第一次檢測,則會出現另外一個線程返回為初始化對象的情況。
樣例4:(面試八股的重點)
class Singleton4 {
public:static Singleton4 * GetInstance() {Singleton4* tmp = _instance.load(std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_acquire);if(tmp == nullptr) {std::lock_guard<std::mutex> lock(_mutex);tmp = _instance.load(std::memory_order_relaxed);if(tmp == nullptr) {tmp = new Singleton4();std::atomic_thread_fence(std::memory_order_release);_instance.store(tmp, std::memory_order_relaxed);atexit(Destructor);}}return tmp;}
private:static void Destructor() {Singleton4* tmp = _instance.load(std::memory_order_relaxed);if(nullptr != tmp) {delete tmp;}}Singleton4() {}~Singleton4() {std::cout << "~Singleton4()\n";}Singleton4(const Singleton4 &) = delete;Singleton4& operator = (const Singleton4&) = delete;Singleton4(Singleton4 &&) = delete;Singleton4& operator = (Singleton4 &&) = delete;static std::atomic<Singleton4*> _instance;static std::mutex _mutex;
};
std::atomic<Singleton4*> Singleton4::_instance;
std::mutex Singleton4::_mutex;
使用內存屏障和原子操作來解決指令重排的問題
內存屏障:
- 作用:
強制限制指令重排,并確保內存操作的可見性(即一個線程的寫入對其他線程立即可見)。 - 類型:
- 獲取屏障(acquire fence):
后續讀/寫操作不會重排到屏障前,且能讀取其他線程的釋放操作結果。 - 釋放屏障(release fence):
前面的讀/寫操作不會重排到屏障后,且保證當前線程的寫入對其他線程可見。
- 獲取屏障(acquire fence):
- 代碼中的應用:
- 獲取屏障:確保
if(tmp == nullptr)
之后的代碼能看到其他線程的完整初始化結果。 - 釋放屏障:確保
new
的構造操作完成后,再存儲指針到_instance
。
- 獲取屏障:確保
原子操作
- 定義:
不可分割的操作,保證對變量的讀寫要么完全執行,要么不執行,不會出現中間狀態。 - 內存順序(Memory Order):
memory_order_relaxed
:僅保證原子性,無同步或順序約束(允許指令重排)。memory_order_acquire
/release
:與屏障配合,實現同步語義。
- 代碼中的應用:
_instance
被聲明為std::atomic<Singleton4*>
,確保其讀寫是原子的,避免數據競爭。
原子操作詳情參考:C++八股 —— 原子操作-CSDN博客
樣例5
class Singleton5 {
public:static Singleton5* GetInstance() {static Singleton5 instance;return &instance;}
private:Singleton5() {}~Singleton5() {std::cout << "~Singleton5()\n";}Singleton5(const Singleton5 &) = delete;Singleton5& operator = (const Singleton5&) = delete;Singleton5(Singleton5 &&) = delete;Singleton5& operator = (Singleton5 &&) = delete;
};
靜態局部變量具備單例的全部三個特性
最簡單也是最推薦的版本
樣例6
template<typename T>
class Singleton {
public:static T* GetInstance() {static T instance;return &instance;}
protected:Singleton() {}virtual ~Singleton() {std::cout << "~Singleton()\n";}
private:Singleton(const Singleton &) = delete;Singleton& operator = (const Singleton&) = delete;Singleton(Singleton &&) = delete;Singleton& operator = (Singleton &&) = delete;
};class DesignPattern : public Singleton<DesignPattern> {friend class Singleton<DesignPattern>;
private:DesignPattern() {}~DesignPattern() {std::cout << "~DesignPattern()\n";}
};
類模板封裝單例的三個特性,使用時直接繼承即可。
- 基類構造和析構函數設置為
protected
是因為需要其對子類時可見的 - 友元是為了讓基類能訪問子類的構造析構函數