單例模式要求全局僅有一個實例,且提供全局訪問點。多線程下的難點在於:防止多線程並發創建多個實例,同時保證性能(避免每次獲取都加鎖)。
class Singleton {
static Singleton instance; // 靜態存儲區,程序啟動時構造
public:
static Singleton& GetInstance() { return instance; }
// 禁用拷貝/移動
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
Singleton Singleton::instance;
優點:無鎖、無競態、編譯器保證初始化順序。
缺點:程序啟動即構造,即使從未使用也佔用資源;構造依賴其他單例時可能發生「靜態初始化順序災難」。
class Singleton {
static Singleton* instance;
static std::mutex mtx;
public:
static Singleton* GetInstance() {
if (instance == nullptr) { // 第一次檢查(無鎖)
std::lock_guard lk(mtx);
if (instance == nullptr) { // 第二次檢查(持鎖)
instance = new Singleton();
}
}
return instance;
}
// ... 禁用拷貝、私有構造
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
關鍵:instance 必須聲明為 std::atomic<Singleton*> 並配合 memory_order_acquire/release,否則 CPU 亂序執行可能導致其他線程看到半初始化的對象(指針非空但構造函數未執行完)。C++11 前極易寫錯。
class Singleton {
public:
static Singleton& GetInstance() {
static Singleton instance; // C++11 保證線程安全初始化(magic static)
return instance;
}
// ... 禁用拷貝、私有構造
private:
Singleton() = default;
};
優點:代碼極簡、零鎖開銷(首次初始化內部用原子操作)、按需構造、自動析構、彌補了靜態初始化順序問題。
缺點:析構順序仍為逆構造順序,若析構時依賴其他單例需謹慎。
class Singleton {
static std::unique_ptr<Singleton> instance;
static std::once_flag flag;
public:
static Singleton* GetInstance() {
std::call_once(flag, []{ instance.reset(new Singleton()); });
return instance.get();
}
// ...
};
std::unique_ptr<Singleton> Singleton::instance;
std::once_flag Singleton::flag;
適用於初始化邏輯複雜(如需讀取配置、連接數據庫)且必須保證只執行一次的場景。
template <typename T>
class SingletonBase {
public:
static T& Instance() {
static T instance; // 派生類構造
return instance;
}
protected:
SingletonBase() = default;
~SingletonBase() = default;
SingletonBase(const SingletonBase&) = delete;
SingletonBase& operator=(const SingletonBase&) = delete;
};
// 使用
class MyService : public SingletonBase<MyService> {
friend class SingletonBase<MyService>;
MyService() { /* 初始化 */ }
};
現代 C++(C++11 及以上)項目,**首選「局部靜態變量」寫法**(方案 3),既標準又高效。僅當初始化邏輯極其複雜或需延遲到首次調用時顯式報錯時,才考慮 std::call_once。請堅決拋棄手寫 DCLP,除非你精通 C++ 內存模型並能正確使用 std::atomic 與內存序。