單例模式的構造函數是私有的,目的是讓用戶無法直接new出實例,而只有通過其他的接口來獲取實例,單例模式在這里作文章,使得多次獲取到的實例,都是同一個實例。
單例模式,分為餓漢式單例 和 懶漢式單例。
先把本類對象所需內存在main函數執行前就new出來,這是餓漢式單例。
個人思考:
為什么餓漢式不獨霸天下,還有什么必要去研究使用cpp11上支持的雙檢查鎖機制(這是懶漢式,用到類實例時才去申請內存)?就因為餓漢式單例事先就占用了一些類內存?反正遲早都要占用內存啊。
或者說,餓漢式單例有什么缺陷。
餓漢式單例:
個人理解:
優點: 編程上使用很簡單
缺點: 整個程序運行期間會一直占用內存,不可以在程序運行期間將其delete。
餓漢式單例模式的重要特點是運用了全局對象的構造過程先於main函數執行之前的特點。
如果程序運行期將實例化的單例對象delete之后,如果有再次創建該單例對象的需求,
正因為餓漢式單例的上述特點,將無法達到滿意的目標效果(目標效果是支持線程安全的單例模式)。
因為程序不可能重新從main函數前再重頭執行一次(在嵌入式平台,只有設備重新上電了)。
即: 餓漢式單例模式不支持動態創建、銷毀單例對象。
普通的懶漢式單例 動態支持的單例對象的申請和釋放
class Singleton{
private:
Singleton();
Singleton(const Singleton& other);
public:
static Singleton* getInstance();
static Singleton* 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) { // 這句代碼並不是多余的,有其作用
m_instance = new Singleton();
}
}
return m_instance;
}
reorder詳解:
假設某個時刻:
線程A 執行到m_instance = new Singleton(); 並且已經完成步驟1和 步驟3, 但是步驟2尚未執行,也就是說,雖然此時m_instance已經不是NULL,但是其
所指向的內存尚未完成構造。
此時,線程B被調度,執行getInstance(),進入上述函數內部,先判斷if(m_instance==nullptr)(注意,這句代碼是未上鎖的,所以B線程可以執行),由於此時m_instance已經不是NULL,所以該函數即將退出,
線程B認為自己已經獲取到了該單實例的句柄,接下來就很有可能使用該單實例的句柄進行操作。 顯然,這不是線程安全的。
另外,解釋下第二個if判斷為什么不是多余的:
線程A有可能在執行第一個if判斷后,立即被調度到線程B執行,而此時線程A尚未執行到Lock lock;的這句上鎖代碼。m_instance被線程B實例化(完成了單例模式整個過程),
再次調度回線程A時,線程A繼續執行上鎖代碼,此時,有必要再次判斷m_instance指針是否為空,如果已經是非空,則不能執行單例類的構造和賦值。
上述的雙檢查鎖的代碼,整體代碼邏輯是沒問題的,雖然是線程非安全的,但這不是程序員能夠解決的了。究其原因,此處線程非安全是因為reorder機制。
所以,我們程序員需要借助編譯器的新特性才能解決該問題。
線程安全的雙檢查鎖 : 從支持C++ 11特性的編譯器開始,提供了通用的跨平台實現。
//線程安全的雙檢查鎖 -- 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);
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;
}
使用cpp11特性支持的雙檢查方式的懶漢式單例不是必須的,只是這種方式是專用於高並發場景下的,滿足高並發要求(ps:這種方式一定是線程安全的)。
補充點:
全局變量的構造時機,和main函數被執行的時機。
全局變量的構造,這是crt (c run time)做的事情,它保證全局變量初始化在main之前運行。
編寫本博客參考過的博客:
https://www.cnblogs.com/goodAndyxublog/p/11356402.html
.