一、概念
保證一個類僅有一個實例,並提供一個該實例的全局訪問點。—《設計模式》GoF
二、動機
在軟件系統中,經常有這樣一個特殊的類,必須保證它們在系統中只存在一個示例,才能確保他們的邏輯正確性、以及良好的效率。如何繞過常規的構造器,提供一種機制來保證一個類只有一個實例?工廠模式繞過new是爲了避開緊耦合,單例模式避開new,是解決性能問題這個應該類設計者的責任,而不是使用者的責任。解決方案:
-
將構造函數設置爲私有的;
-
提供一個全局的靜態方法;
-
定義一個靜態指針,指向本類的變量的靜態變量指針。
三、單例模式的實現
class Singleton {
private:
Singleton();
Singleton(const Singleton& other);
public:
static Singleton* getInstance(); // 靜態成員是類的成員而不是對象的成員
static Singleton* m_instance;
};
// 靜態成員需要在類外初始化
Singleton* Singleton::m_instance = nullptr;
// 線程非安全版本
Singleton* Singleton::getInstance() { // 由於是在類的外部定義,也是靜態方法,不用加static
if (m_instance == nullptr) {
// 下面這行代碼可以被多個線程執行,因此不是線程安全的
m_instance = new Singleton();
}
return m_instance;
}
// 線程安全版本,但鎖的代價過高
Singleton* Singleton::getInstance() {
// 鎖的局部變量,會在函數結束時自動釋放,我們也可以設置爲全局變量鎖。
// 局部變量鎖可用是因爲這個函數是靜態方法,所以大家獲取的還是一個鎖
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
// 雙檢查鎖,但由於內存讀寫reorder不安全
Singleton* Singleton::getInstance() {
if(m_instance==nullptr) {
Lock lock;
if (m_instance == nullptr) {
// 分配內存->調用構造函數->賦值,但是編譯器可能對這一過程進行優化,所以內存讀寫reorder不安全
m_instance = new Singleton();
}
}
return m_instance;
}
// C++ 11版本之後的跨平臺實現 (volatile)
std::atomic<Singleton*> Singleton::m_instance; // 原子對象
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed); // 屏蔽編譯器reorder
std::atomic_thread_fence(std::memory_order_acquire); // 獲取內存fence
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex); // 鎖的設置
tmp = m_instance.load(std::memory_order_relaxed); // 取變量出來
if (tmp == nullptr) {
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release); // 釋放內存fence
m_instance.store(tmp, std::memory_order_relaxed); // 存放變量回去
}
}
return tmp;
}
四、類圖結構
五、要點總結
-
Singleton模式中的實例構造器可以設置爲protected以允許子類派生。
-
Singleton模式一般不要支持拷貝構造函數和Clone接口,因爲這有可能會導致多個對象實例,與Singleton模式的初衷相違背。
-
如何實現多線程環境下安全的Singleton?注意對雙檢查鎖的正確實現。