1. 程式人生 > 程式設計 >C++ 執行緒安全的單例模式總結

C++ 執行緒安全的單例模式總結

什麼是執行緒安全?

在擁有共享資料多條執行緒並行執行的程式中,執行緒安全的程式碼會通過同步機制保證各個執行緒都可以正常且正確的執行,不會出現資料汙染等意外情況。


如何保證執行緒安全?

  1. 共享的資源加把,保證每個資源變數每時每刻至多被一個執行緒佔用。
  2. 讓執行緒也擁有資源,不用去共享程式中的資源。如: 使用threadlocal可以為每個執行緒的維護一個私有的本地變數。

什麼是單例模式?

單例模式指在整個系統生命週期裡,保證一個類只能產生一個例項,確保該類的唯一性

單例模式分類

單例模式可以分為懶漢式餓漢式,兩者之間的區別在於建立例項的時間不同

  • 懶漢式:指系統執行中,例項並不存在,只有當需要使用該例項時,才會去建立並使用例項。(這種方式要考慮執行緒安全)
  • 餓漢式:指系統一執行,就初始化建立例項,當需要時,直接呼叫即可。(本身就執行緒安全,沒有多執行緒的問題)

單例類特點

  • 建構函式和解構函式為private型別,目的禁止外部構造和析構
  • 拷貝構造和賦值建構函式為private型別,目的是禁止外部拷貝和賦值,確保例項的唯一性
  • 類裡有個獲取例項的靜態函式,可以全域性訪問

01 普通懶漢式單例 ( 執行緒不安全 )

///////////////////  普通懶漢式實現 -- 執行緒不安全 //////////////////
#include <iostream> // std::cout
#include <mutex>    // std::mutex
#include <pthread.h> // pthread_create
class SingleInstance { public: // 獲取單例物件 static SingleInstance *GetInstance(); // 釋放單例,程式退出時呼叫 static void deleteInstance(); // 列印單例地址 void Print(); private: // 將其構造和析構成為私有的,禁止外部構造和析構 SingleInstance(); ~SingleInstance(); // 將其拷貝構造和賦值構造成為私有函式,禁止外部拷貝和賦值 SingleInstance(const
SingleInstance &signal); const SingleInstance &operator=(const SingleInstance &signal); private: // 唯一單例物件指標 static SingleInstance *m_SingleInstance; }; //初始化靜態成員變數 SingleInstance *SingleInstance::m_SingleInstance = NULL; SingleInstance* SingleInstance::GetInstance() { if (m_SingleInstance == NULL) { m_SingleInstance = new (std::nothrow) SingleInstance; // 沒有加鎖是執行緒不安全的,當執行緒併發時會建立多個例項 } return m_SingleInstance; } void SingleInstance::deleteInstance() { if (m_SingleInstance) { delete m_SingleInstance; m_SingleInstance = NULL; } } void SingleInstance::Print() { std::cout << "我的例項記憶體地址是:" << this << std::endl; } SingleInstance::SingleInstance() { std::cout << "建構函式" << std::endl; } SingleInstance::~SingleInstance() { std::cout << "解構函式" << std::endl; } /////////////////// 普通懶漢式實現 -- 執行緒不安全 ////////////////// // 執行緒函式 void *PrintHello(void *threadid) { // 主執行緒與子執行緒分離,兩者相互不干涉,子執行緒結束同時子執行緒的資源自動回收 pthread_detach(pthread_self()); // 對傳入的引數進行強制型別轉換,由無型別指標變為整形數指標,然後再讀取 int tid = *((int *)threadid); std::cout << "Hi,我是執行緒 ID:[" << tid << "]" << std::endl; // 列印例項地址 SingleInstance::GetInstance()->Print(); pthread_exit(NULL); } #define NUM_THREADS 5 // 執行緒個數 int main(void) { pthread_t threads[NUM_THREADS] = {0}; int indexes[NUM_THREADS] = {0}; // 用陣列來儲存i的值 int ret = 0; int i = 0; std::cout << "main() : 開始 ... " << std::endl; for (i = 0; i < NUM_THREADS; i++) { std::cout << "main() : 建立執行緒:[" << i << "]" << std::endl; indexes[i] = i; //先儲存i的值 // 傳入的時候必須強制轉換為void* 型別,即無型別指標 ret = pthread_create(&threads[i],NULL,PrintHello,(void *)&(indexes[i])); if (ret) { std::cout << "Error:無法建立執行緒," << ret << std::endl; exit(-1); } } // 手動釋放單例項的資源 SingleInstance::deleteInstance(); std::cout << "main() : 結束! " << std::endl; return 0; } 複製程式碼

普通懶漢式單例執行結果:

從執行結果可知,單例建構函式建立了兩個個,記憶體地址分別為0x7f3c980008c00x7f3c900008c0,所以普通懶漢式單例只適合單程式不適合多執行緒,因為是執行緒不安全的。


02 加鎖的懶漢式單例 ( 執行緒安全 )

///////////////////  加鎖的懶漢式實現  //////////////////
class SingleInstance
{

public:
    // 獲取單例項物件
    static SingleInstance *&GetInstance();

    //釋放單例項,程式退出時呼叫
    static void deleteInstance();
	
    // 列印例項地址
    void Print();

private:
    // 將其構造和析構成為私有的,禁止外部拷貝和賦值
    SingleInstance(const SingleInstance &signal);
    const SingleInstance &operator=(const SingleInstance &signal);

private:
    // 唯一單例項物件指標
    static SingleInstance *m_SingleInstance;
    static std::mutex m_Mutex;
};

//初始化靜態成員變數
SingleInstance *SingleInstance::m_SingleInstance = NULL;
std::mutex SingleInstance::m_Mutex;

SingleInstance *&SingleInstance::GetInstance()
{

    //  這裡使用了兩個 if判斷語句的技術稱為雙檢鎖;好處是,只有判斷指標為空的時候才加鎖,
    //  避免每次呼叫 GetInstance的方法都加鎖,鎖的開銷畢竟還是有點大的。
    if (m_SingleInstance == NULL) 
    {
        std::unique_lock<std::mutex> lock(m_Mutex); // 加鎖
        if (m_SingleInstance == NULL)
        {
            m_SingleInstance = new (std::nothrow) SingleInstance;
        }
    }

    return m_SingleInstance;
}

void SingleInstance::deleteInstance()
{
    std::unique_lock<std::mutex> lock(m_Mutex); // 加鎖
    if (m_SingleInstance)
    {
        delete m_SingleInstance;
        m_SingleInstance = NULL;
    }
}

void SingleInstance::Print()
{
	std::cout << "我的例項記憶體地址是:" << this << std::endl;
}

SingleInstance::SingleInstance()
{
    std::cout << "建構函式" << std::endl;
}

SingleInstance::~SingleInstance()
{
    std::cout << "解構函式" << std::endl;
}
///////////////////  加鎖的懶漢式實現  //////////////////
複製程式碼

加鎖的懶漢式單例的執行結果:

從執行結果可知,只建立了一個例項,記憶體地址是0x7f28b00008c0,所以加了互斥鎖的普通懶漢式是執行緒安全的


03 內部靜態變數的懶漢單例(C++11 執行緒安全)

///////////////////  內部靜態變數的懶漢實現  //////////////////
class Single
{

public:
    // 獲取單例項物件
    static Single &GetInstance();
	
	// 列印例項地址
    void Print();

private:
    // 禁止外部構造
    Single();

    // 禁止外部析構
    ~Single();

    // 禁止外部複製構造
    Single(const Single &signal);

    // 禁止外部賦值操作
    const Single &operator=(const Single &signal);
};

Single &Single::GetInstance()
{
    // 區域性靜態特性的方式實現單例項
    static Single signal;
    return signal;
}

void Single::Print()
{
    std::cout << "我的例項記憶體地址是:" << this << std::endl;
}

Single::Single()
{
    std::cout << "建構函式" << std::endl;
}

Single::~Single()
{
    std::cout << "解構函式" << std::endl;
}
///////////////////  內部靜態變數的懶漢實現  //////////////////
複製程式碼

內部靜態變數的懶漢單例的執行結果:

-std=c++0x編譯是使用了C++11的特性,在C++11內部靜態變數的方式裡是執行緒安全的,只建立了一次例項,記憶體地址是0x6016e8,這個方式非常推薦,實現的程式碼最少!

[root@lincoding singleInstall]#g++  SingleInstance.cpp -o SingleInstance -lpthread -std=c++0x
複製程式碼


04 餓漢式單例 (本身就執行緒安全)

////////////////////////// 餓漢實現 /////////////////////
class Singleton
{
public:
    // 獲取單例項
    static Singleton* GetInstance();

    // 釋放單例項,程式退出時呼叫
    static void deleteInstance();
    
    // 列印例項地址
    void Print();

private:
    // 將其構造和析構成為私有的,禁止外部構造和析構
    Singleton();
    ~Singleton();

    // 將其拷貝構造和賦值構造成為私有函式,禁止外部拷貝和賦值
    Singleton(const Singleton &signal);
    const Singleton &operator=(const Singleton &signal);

private:
    // 唯一單例項物件指標
    static Singleton *g_pSingleton;
};

// 程式碼一執行就初始化建立例項 ,本身就執行緒安全
Singleton* Singleton::g_pSingleton = new (std::nothrow) Singleton;

Singleton* Singleton::GetInstance()
{
    return g_pSingleton;
}

void Singleton::deleteInstance()
{
    if (g_pSingleton)
    {
        delete g_pSingleton;
        g_pSingleton = NULL;
    }
}

void Singleton::Print()
{
    std::cout << "我的例項記憶體地址是:" << this << std::endl;
}

Singleton::Singleton()
{
    std::cout << "建構函式" << std::endl;
}

Singleton::~Singleton()
{
    std::cout << "解構函式" << std::endl;
}
////////////////////////// 餓漢實現 /////////////////////
複製程式碼

餓漢式單例的執行結果:

從執行結果可知,餓漢式在程式一開始就建構函式初始化了,所以本身就執行緒安全的


特點與選擇

  • 懶漢式是以時間換空間,適應於訪問量較時;推薦使用內部靜態變數的懶漢單例,程式碼量少
  • 餓漢式是以空間換時間,適應於訪問量較時,或者執行緒比較多的的情況