C++單例模式詳解


參考文章:設計模式(一)–深入單例模式(涉及線程安全問題)。代碼全,解釋清楚。
本篇博客編譯時,需要使用C++11標準。

一、單例模式概念以及優缺點

(1)定義:

要求一個類只能生成一個對象,所有對象對它的依賴相同。
(2)優點:

  • 只有一個實例,減少內存開支。應用在一個經常被訪問的對象上。
  • 減少系統的性能開銷,應用啓動時,直接產生一單例對象,用永久駐留內存的方式。
  • 避免對資源的多重佔用。
  • 可在系統設置全局的訪問點,優化和共享資源訪問。

(3)缺點:

  • 一般沒有接口,擴展困難。原因:接口對單例模式沒有任何意義;要求“自行實例化”,並提供單一實例,接口或抽象類不可能被實例化。(當然,單例模式可以實現接口、被繼承,但需要根據系統開發環境判斷)
  • 單例模式對測試是不利的。如果單例模式沒完成,是不能進行測試的。
  • 單例模式與單一職責原則有衝突。原因:一個類應該只實現一個邏輯,而不關心它是否是單例,是不是要單例取決於環境;單例模式把“要單例”和業務邏輯融合在一個類。

(4)使用場景:

  • 要求生成唯一序列化的環境.
  • 項目需要的一個共享訪問點或共享的數據點.
  • 創建一個對象需要消耗資源過多的情況。如:要訪問IO和 數據庫等資源。
  • 需要定義大量的靜態常量和靜態方法(如工具類)的環境。可以採用單例模式或者直接聲明static的方式。

(5)注意事項:

  • 類中其他方法,儘量是static。

二、懶漢式單例模式

1、原始懶漢式單例模式

懶漢式單例就是需要使用這個單例對象的時候纔去創建這個單例對象。

#include <iostream>
#include <stdexcept>
#include <thread>
#include <mutex>

#ifdef WIN32
#include <windows.h>
#define SLEEP(x) Sleep(x)
#else
#include <unistd.h>
#define SLEEP(x) usleep(x*1000)
#endif

using namespace std;

//懶漢式單例
class Singleton {
private:
    static Singleton *singleton;
    Singleton() = default;
    Singleton(const Singleton& s) = default;
    Singleton& operator=(const Singleton& s) = default;
public:
    static Singleton* getInstance(){
        if (Singleton::singleton == nullptr){
            SLEEP(10);//休眠,模擬創建實例的時間
            singleton = new Singleton();
        }
        return singleton;
    }
};

// 必須在類外初始化
Singleton* Singleton::singleton = nullptr;
// 定義一個互斥鎖
mutex m;

void print_address(){
    // 獲取實例
    Singleton* singleton1 = Singleton::getInstance();
    // 打印singleton1地址
    m.lock(); // 鎖住,保證只有一個線程在打印地址
    cout<<singleton1<<endl;
    m.unlock();// 解鎖
}

int main(){
    thread threads[10];

    // 創建10個線程
    for (auto&t : threads)
        t = thread(print_address);
    // 對每個線程調用join,主線程等待子線程完成運行
    for (auto&t : threads)
        t.join();
}

運行結果:

0x7ff280000b20
0x7ff298000b20
0x7ff290000b20
0x7ff288000b20
0x7ff278000b20
0x7ff260000b20
0x7ff268000b20
0x7ff264000b20
0x7ff258000b20
0x7ff270000b20

可以看出,結果裏面有好幾個不同地址的示例! 所以,這種單例模式不是線程安全的。原因是,當幾個線程同時執行到語句if (Singleton::singleton == nullptr)時,singleton都還沒有被創建,所以就重複創建了幾個實例。

2、線程安全的單例模式

爲了編寫線程安全的單例模式,可以鎖住getInstance函數,保證同時只有一個線程訪問getInstance函數(爲了節省篇幅,相同的部分不在代碼中再次給出)。

using namespace std;

//線程安全的懶漢式單例
mutex m1;
class Singleton {
private:
    static Singleton *singleton;
    Singleton() = default;
    Singleton(const Singleton& s) = default;
    Singleton& operator=(const Singleton& s) = default;
public:
    static Singleton* getInstance() {
        m1.lock(); // 加鎖,保證只有一個線程在訪問下面的語句
        if (Singleton::singleton == nullptr){
            SLEEP(10); //休眠,模擬創建實例的時間
            singleton = new Singleton();
        }
        m1.unlock();//解鎖
        return singleton;
    }
};

運行輸出:

0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20
0x7fcb80000b20

可以發現,所有線程獲取到的實例的地址都相同。整個程序中只有一個Singleton實例。原因是進入getInstance函數之後,立馬鎖住創建實例的語句,保證只有一個線程在訪問創建實例的代碼。

3、鎖住初始化實例語句的方式

僅僅對創建實例的語句進行加鎖,是否是線程安全的呢?

//線程安全的懶漢式單例
mutex m1;
class Singleton {
private:
    static Singleton *singleton;
    Singleton() = default;
    Singleton(const Singleton& s) = default;
    Singleton& operator=(const Singleton& s) = default;
public:
    static Singleton* getInstance() {
        if (Singleton::singleton == nullptr){
            SLEEP(100); //休眠,模擬創建實例的時間
            m1.lock();  // 加鎖,保證只有一個線程在創建實例
            singleton = new Singleton();
            m1.unlock();//解鎖
        }
        return singleton;
    }
};

運行輸出:

0x7ff7f0000b20
0x7ff7e8000b20
0x7ff7f0000f50
0x7ff7ec000b20
0x7ff7e0000b20
0x7ff7ec000b40
0x7ff7f0000f70
0x7ff7e0000b40
0x7ff7f0000f90
0x7ff7ec000b60

這種方式不是線程安全的。 因爲當線程同時執行到語句if (Singleton::singleton == nullptr)時,singleton都還沒有被創建,故會條件爲真,多個線程都會創建實例,儘管不是同時創建。

4、鎖住初始化實例語句之後再次檢查實例是否被創建

//線程安全的懶漢式單例
mutex m1;
class Singleton {
private:
    static Singleton *singleton;
    Singleton() = default;
    Singleton(const Singleton& s) = default;
    Singleton& operator=(const Singleton& s) = default;
public:
    static Singleton* getInstance() {
        if (Singleton::singleton == nullptr){
            SLEEP(100); //休眠,模擬創建實例的時間
            m1.lock();  // 加鎖,保證只有一個線程在訪問線程內的代碼
            if (Singleton::singleton == nullptr) { //再次檢查
                singleton = new Singleton();
            }
            m1.unlock();//解鎖
        }
        return singleton;
    }
};

運行輸出:

0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20
0x7f0bc4000b20

可以看出,這種方式是線程安全的。並且沒有第2種代碼簡潔。

三、餓漢式單例

先實例化該單例類,而不是像之前一樣初始化爲空指針。

using namespace std;

//線程安全的餓漢式單例
class Singleton {
private:
    static Singleton *singleton;
    Singleton() try{
        // 構造本單利模式的代碼
    }catch (exception& e){
        cout << e.what() << endl;
        // 在這裏處理可能的異常情況
        throw;
    }

    Singleton(const Singleton& s) = default;
    Singleton& operator=(const Singleton& s) = default;
public:
    static Singleton* getInstance() {
        return singleton;
    }
};

// 必須在類外初始化
Singleton* Singleton::singleton = new Singleton();

// 定義一個互斥鎖
mutex m;

運行輸出:

0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70
0x56362e1fae70

可以看出singleton的實例確實只有一個。餓漢式單利會在程序開始之前就被創建,所以是線程安全的。由於創建的單例是在全局變量區,所以需要處理構造函數中可能出現的異常:

Singleton() try{
        // 構造本單利模式的代碼
    }catch (exception& e){
        cout << e.what() << endl;
        // 在這裏處理可能的異常情況
        throw;
    }

這裏涉及到的知識是:很少有人知道的c++中的try塊函數

至於參考博客中提到的序列化與反序列化問題,不在本博客中做討論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章