C++ 線程安全的單例模式總結

什麼是線程安全?

在擁有共享數據多條線程並行執行的程序中,線程安全的代碼會經過同步機制保證各個線程均可以正常且正確的執行,不會出現數據污染等意外狀況。ios


如何保證線程安全?

  1. 共享的資源加把,保證每一個資源變量每時每刻至多被一個線程佔用。
  2. 讓線程也擁有資源,不用去共享進程中的資源。如: 使用threadlocal能夠爲每一個線程的維護一個私有的本地變量。

什麼是單例模式?

單例模式指在整個系統生命週期裏,保證一個類只能產生一個實例,確保該類的惟一性c++

單例模式分類

單例模式能夠分爲懶漢式餓漢式,二者之間的區別在於建立實例的時間不一樣shell

  • 懶漢式:指系統運行中,實例並不存在,只有當須要使用該實例時,纔會去建立並使用實例。(這種方式要考慮線程安全)
  • 餓漢式:指系統一運行,就初始化建立實例,當須要時,直接調用便可。(自己就線程安全,沒有多線程的問題)

單例類特色

  • 構造函數和析構函數爲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();
    ~SingleInstance();

    // 將其拷貝構造和賦值構形成爲私有函數, 禁止外部拷貝和賦值
    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;
}
////////////////////////// 餓漢實現 /////////////////////

餓漢式單例的運行結果:

從運行結果可知,餓漢式在程序一開始就構造函數初始化了,因此自己就線程安全的
餓漢式單例的運行結果併發


特色與選擇

  • 懶漢式是以時間換空間,適應於訪問量較時;推薦使用內部靜態變量的懶漢單例,代碼量少
  • 餓漢式是以空間換時間,適應於訪問量較時,或者線程比較多的的狀況
相關文章
相關標籤/搜索