1. 程式人生 > >設計模式:單例模式-懶漢模型和餓漢模型

設計模式:單例模式-懶漢模型和餓漢模型

什麼是單例模式?

保證一個類只有一個例項,並提供一個訪問它的全域性訪問點。首先,需要保證一個類只有一個例項;在類中,要構造一個例項,就必須呼叫類的建構函式,如此,為了防止在外部呼叫類的建構函式而構造例項,需要將建構函式的訪問許可權標記為protected或private;最後,需要提供要給全域性訪問點,就需要在類中定義一個static函式,返回在類內部唯一構造的例項。

要求:

(1)單例類保證全域性只有一個自行建立的例項物件
(2)單例類提供獲取這個唯一例項的介面

分類:

(1)懶漢模式:只有當呼叫物件時才建立例項,相對而言,實現程式碼較複雜但實用性強。
(2)餓漢模式:在執行main函式開始就呼叫建立物件。這種實現方式簡單高效,但適用性受到限制。在main函式之前建立執行緒可能會出現問題,在動態庫里程序可能崩潰。

懶漢模式

#include<iostream>
#include<mutex>
#include<Windows.h>
using namespace std;

class Singleton
{
public:
    static Singleton* GetInstance()//獲取唯一例項的介面函式
    {
        if (_inst == NULL)//建立例項
        {
            _inst = new Singleton;
        }
        return _inst;
    }

    void
Print() { cout << "Singleton: " << this << endl; } private: int _a; Singleton()//將建構函式設為私有,防止在類外建立例項 :_a(0) {} static Singleton* _inst;//將指向例項的指標定義為靜態私有,這樣定義靜態成員函式獲取物件例項 }; Singleton* Singleton::_inst = NULL;//類外初始化靜態成員 void Test1() { Singleton::GetInstance()->Print(); Singleton::GetInstance()->Print(); Singleton::GetInstance()->Print(); } int
main() { Test1(); system("pause"); return 0; }

這裡寫圖片描述

看上述結果,的確是只產生了一個例項物件,但如果在類外拷貝或是賦值呢?這又如何保證只產生一個例項呢?所以,我們還應該做到防拷貝。,即將拷貝構造和賦值運算子的過載宣告為私有(類似於string 類防拷貝的實現)。

class Singleton
{
public:
    static Singleton* GetInstance()//獲取唯一例項的介面函式
    {
        if (_inst == NULL)//建立例項
        {
            _inst = new Singleton;
        }
        return _inst;
    }

    void Print()
    {
        cout << "Singleton: " << this << endl;
    }

private:
    int _a;
    Singleton()//將建構函式設為私有,防止在類外建立例項
        :_a(0)
    {}

    Singleton(const Singleton&);
    Singleton& operator = (const Singleton&);
    static Singleton* _inst;//將指向例項的指標定義為靜態私有,這樣定義靜態成員函式獲取物件例項 
};

Singleton* Singleton::_inst = NULL;//類外初始化靜態成員

上述過程我們看似已經實現了單例模式只能建立一個例項且只有一個唯一的介面函式,但是,這只是在單執行緒環境下,而如果有多執行緒同時訪問的時候就會出現執行緒安全問題。在linux環境下,我們使用pthread_mutex加鎖保證執行緒安全,這裡我們同樣採用加鎖機制。

class Singleton
{
public:
    static Singleton* GetInstance()//獲取唯一例項的介面函式
    {
        _mtx.lock();//加鎖,加鎖期間其他執行緒不能訪問臨界區
        if (_inst == NULL)//建立例項
        {
            _inst = new Singleton;
        }
        _mtx.unlock();//解鎖
        return _inst;
    }

    void Print()
    {
        cout << "Singleton: " << this << endl;
    }

private:
    int _a;
    Singleton()//將建構函式設為私有,防止在類外建立例項
        :_a(0)
    {}

    Singleton(const Singleton&);
    Singleton& operator = (const Singleton&);
    static Singleton* _inst;//將指向例項的指標定義為靜態私有,這樣定義靜態成員函式獲取物件例項 
    static mutex _mtx; // 保證執行緒安全的互斥鎖
};

mutex Singleton::_mtx;//_mtx會呼叫mutex預設的無參建構函式,所以不用初始化
Singleton* Singleton::_inst = NULL;//類外初始化靜態成員

而上述方案又存在一個問題,當我們加鎖後申請資源時程式崩潰或是異常終止,而我們還沒來得及釋放鎖資源,這時就會導致異常退出的執行緒一直擁有鎖資源,而其他執行緒無法訪問臨界資源。這樣既造成了死鎖問題。

如何解決死鎖問題呢?在學習只能智慧指標時,我們瞭解過RAII機制(資源分配初始化), RAII利用建構函式分配並初始化資源,利用解構函式釋放資源,保證原子性訪問。所以這裡我們使用RAII 機制自己“造輪子”。

class Lock
{
public:
    Lock(mutex& mtx)
        :_mtx(mtx)
    {
        _mtx.lock();
    }

    ~Lock()
    {
        _mtx.unlock();
    }
protected:
    Lock(const Lock&);
    Lock& operator = (const Lock&);
private:
    mutex& _mtx;
};


class Singleton
{
public:
    static Singleton* GetInstance()//獲取唯一例項的介面函式
    {
        Lock lock(_mtx);//RAII機制
        if (_inst == NULL)//建立例項
        {
            _inst = new Singleton;
        }
        return _inst;
    }

    void Print()
    {
        cout << "Singleton: " << this << endl;
    }

private:
    int _a;
    Singleton()//將建構函式設為私有,防止在類外建立例項
        :_a(0)
    {}

    static mutex _mtx; // 保證執行緒安全的互斥鎖
    Singleton(const Singleton&);
    Singleton& operator = (const Singleton&);
    static Singleton* _inst;//將指向例項的指標定義為靜態私有,這樣定義靜態成員函式獲取物件例項 
};

Singleton* Singleton::_inst = NULL;//類外初始化靜態成員
mutex Singleton::_mtx;//_mtx會呼叫mutex預設的無參建構函式,所以不用初始化

而實際上,庫中是有這樣的加鎖機制的,我們可以直接呼叫庫函式進行加鎖。

 lock_guard<mutex>lock(_mtx);

我們的單例模式要求是執行緒安全且高效,現在我們已經實現了執行緒安全,那麼高效如何實現?需要使用雙檢查。,而在上述過程中,我們建立例項時,使用

_inst = new Singleton

這行程式碼,系統執行時,會呼叫operator new 、建構函式及賦值運算子,但在不同的編譯器下,可能會對它進行不同程度的優化,這樣我們就無法保證內部的呼叫順序,因此我們對此優化,並加入記憶體柵欄
那麼既然我們使用了new開闢空間,自然是要delete釋放空間的。在實際專案中,我們其實並不關心單例模式的釋放,因為全域性就一個例項,它一直伴隨著這個軟體的使用,可以說它的生命週期隨軟體。當這個軟體停止了,那這個例項自然也就沒有了。但有些情況是必須我們手動來釋放資源的:
(1)在類中,有一些檔案鎖,檔案控制代碼,資料庫連線等等,這些隨著程式的關閉而不會立即關閉的資源,必須要在程式關閉前,進行手動釋放。
(2)有強迫症的程式設計師(我覺得程式設計師一般都有強迫症的哈哈)

基於以上的分析和不斷優化的過程,我們給出完整版的懶漢模式的實現:

class Singleton
{
public:
    static Singleton* GetInstance()//獲取唯一例項的介面函式
    {
        if (_inst == NULL)//雙檢查機制,只有建立例項的時候才進行加鎖解鎖來提高程式碼效率
        {
            //Lock lock(_mtx);//RAII機制
            lock_guard<mutex>lock(_mtx);
            if (_inst == NULL)//建立例項
            {
                Singleton* tmp = new Singleton;
                MemoryBarrier();//記憶體柵欄
                _inst = tmp;
            }
        }
        return _inst;//返回例項化的唯一物件
    }

    void Print()
    {
        cout << "Singleton: " << _inst << endl;
    }

    static void DellInstance()//在某些情況才需要釋放
    {
        lock_guard<mutex> lock(_mtx);//防止物件被釋放多次

        if (_inst)
        {
            cout << "delete" << endl;
            delete _inst;
            _inst = NULL;//防止野指標的出現
        }
    }

    struct GC
    {
        ~GC()
        {
            DellInstance();
        }
    };
private:
    int _a;
    Singleton()//將建構函式設為私有,防止在類外建立例項
        :_a(0)
    {}

    ~Singleton()
    {

    }
    static mutex _mtx; // 保證執行緒安全的互斥鎖
    Singleton(const Singleton&);
    Singleton& operator = (const Singleton&);
    static Singleton* _inst;//將指向例項的指標定義為靜態私有,這樣定義靜態成員函式獲取物件例項 
};

Singleton* Singleton::_inst = NULL;//類外初始化靜態成員
mutex Singleton::_mtx;//_mtx會呼叫mutex預設的無參建構函式,所以不用初始化
static Singleton::GC gc;

void Test1()
{
    Singleton::GetInstance()->Print();
    Singleton::GetInstance()->Print();
    Singleton::GetInstance()->Print();

    Singleton::DellInstance();
    //atexit(Singleton::DellInstance);//也註冊回撥函式,在main函式之後呼叫析構
}
int main()
{
    Test1();
    system("pause");
    return 0;
}

這裡寫圖片描述

餓漢模式
—-餓漢模式1

class Singleton
{
public:
    static Singleton& GetInstance()
    {
        static Singleton inst;//靜態變數只會建立一次
        return inst;
    }
    void Print()
    {
        cout << "Singleton:" << this << endl;
    }
protected:
    Singleton()
        :_a(0)
    {}
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);

    int _a;
};


void Test1()
{
    Singleton::GetInstance().Print();
    Singleton::GetInstance().Print();
    Singleton::GetInstance().Print();
}

int main()
{
    Test1();
    system("pause");
    return 0;
}

餓漢模式2

#include<assert.h>
class Singleton
{
public:
    static Singleton& GetInstance()
    {
        assert(_inst);
        return *_inst;
    }
    void Print()
    {
        cout << "Singleton:" << _a << endl;
    }
protected:
    Singleton()
        :_a(0)
    {}
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);

    static Singleton* _inst;
    int _a;
};

Singleton* Singleton::_inst = new Singleton;//靜態成員在main函式之前初始化

void Test()
{
    Singleton::GetInstance().Print();
    Singleton::GetInstance().Print();
    Singleton::GetInstance().Print();
}

int main()
{
    Test();
    system("pause");
    return 0;
}

總結:懶漢式的特點是用到例項化的時候才會載入,而餓漢式是一開始就載入了。兩種方案的建構函式和公用方法都是靜態的(static),例項和公用方法又都是私有的(private)。但是餓漢式每次呼叫的時候不用做建立,直接返回已經建立好的例項。這樣雖然節省了時間,但是卻佔用了空間,例項本身為static的,會一直在記憶體中帶著。懶漢式則是判斷,在用的時候才載入,會影響程式的速度。

推薦部落格:單例模式 博主舉的例子有助於大家理解單例模式。