單例模式,reorder詳解,線程安全,雙檢查鎖


 

單例模式的構造函數是私有的,目的是讓用戶無法直接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

 

 

 

.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM