設計模式之Singleton(單件模式)

1、動機

一、在軟件系統中,常常有這樣一個特殊的類,必須保證他們在系統中只存在一個實例,才能保證他們的邏輯正確性,以及良好的效率。
二、如何繞過常規的構造器,提供一種機制來保證一個類只有一個實例;
三、這應該是設計者的責任,而不是使用者的責任;
四、保證一個類僅有一個實例,並提供一個該實例的全局訪問點(GOF)ios

2、實現分析

一、非線程安全的單件模式;c++

#ifndef SINGLETON_H
#define SINGLETON_H
#include<iostream>
class singleton
{
private:
    singleton(int data=0):data(data)
    { std::cout << "建立構造函數" << std::endl; };
    singleton(const singleton&) {};
    ~singleton() {};
    static singleton* point;
    int data;
public:
    static singleton* get_singleton(int);
    void set_singleton(int);
    void get_data();
};
singleton* singleton::point= nullptr;
singleton* singleton::get_singleton(int data = 0)
{
    if (point == nullptr)
    {
        point = new singleton(data);
    }
    return point;
};
void singleton::set_singleton(int data) 
{
    this->data = data;
}
void singleton::get_data() 
{
    std::cout << "the data is : " << this->data<<std::endl;
}
#endif // !SINGLETON_H

咱們實現了最簡單的單例模式,爲了知足僅僅生成一個實例,所以咱們應該爲客戶從新提供一個實例化接口而且屏蔽類的默認構造函數與默認複製構造函數,隨後實現這個實例化接口,咱們能夠看到如下代碼句爲核心:安全

singleton* singleton::get_singleton(int data = 0)
{
    if (point == nullptr)
    {
        point = new singleton(data);
    }
    return point;
};

首先判斷是否已經實例化了這個單一對象,由於實例化這個單一對象的代價是這個對象的靜態成員指針非空,所以可使用這一項進行檢查,這個代碼在單線程的狀況下能夠正確使用。
可是在多線程的狀況下,若線程1執行了if (point == nullptr)被掛起,而線程2繼續執行則會建立一個對象,當線程1再次被激活後又會再建立一個對象,這就形成了以前線程1建立的內存泄漏的問題。
二、單鎖的線程安全的單件模式;
因爲1中的單件模式存在的問題,使用加鎖能夠解決這個問題,代碼以下:多線程

#ifndef SINGLETON_H
#define SINGLETON_H
#include<iostream>
#include<thread>
#include<mutex>
using std::mutex;
using std::lock_guard;
mutex mut;
class singleton
{
private:
    singleton(int data=0):data(data)
    { std::cout << "建立構造函數" << std::endl; };
    singleton(const singleton&) {};
    ~singleton() {};
    static singleton* point;
    int data;
public:
    static singleton* get_singleton(int);
    void set_singleton(int);
    void get_data();
};
singleton* singleton::point= nullptr;
singleton* singleton::get_singleton(int data = 0)
{
    lock_guard<mutex> lock(mut);//上鎖與自動解鎖
    if (point == nullptr)
    {
        point = new singleton(data);
    }
    return point;
};
void singleton::set_singleton(int data) 
{
    this->data = data;
}
void singleton::get_data() 
{
    std::cout << "the data is : " << this->data<<std::endl;
}
#endif // !SINGLETON_H

能夠看到咱們使用了鎖來解決這個問題,這的確在邏輯上達到了線程安全,可是這種寫法依舊存在一個問題,那就是加鎖的代價過高,在下面代碼中函數

singleton* singleton::get_singleton(int data = 0)
{
    lock_guard<mutex> lock(mut);//上鎖與自動解鎖
    if (point == nullptr)
    {
        point = new singleton(data);
    }
    return point;
};

全部的線程都須要進行上鎖與解鎖操做,即便是point != nullptr。這個很不合理,由於僅僅在point == nullptr時候才須要加鎖,考慮到這個緣由須要在point != nullptr不進行加鎖。
三、雙檢查鎖的線程安全(可是因爲內存讀寫reorder,不能用,不安全);
爲了提高效率,在point != nullptr不進行加鎖,那麼寫成以下形式正確嗎?
很明顯是存在問題的,這樣寫的話若全部線程都在point == nullptr掛起直接就讓鎖失效了。優化

singleton* singleton::get_singleton(int data = 0)
{
    if (point == nullptr)
    {
        lock_guard<mutex> lock(mut);//上鎖與自動解鎖
        point = new singleton(data);
    }
    return point;
};

正確的寫法應該爲:ui

singleton* singleton::get_singleton(int data = 0)
{
    if (point == nullptr)
    {
        lock_guard<mutex> lock(mut);//上鎖與自動解鎖
        if(point==nullptr)
            point = new singleton(data);
    }
    return point;
};

在加鎖後進行二次判斷point == nullptr,這樣就可以達到邏輯上的正確了。
可是依舊存在問題:因爲new的操做與底層編譯器有關,當底層的編譯器按照malloc->構造函數->賦值給point的順序來執行,可是某些編譯器爲了優化會採用malloc->賦值給point->構造函數的順序來執行,這就是reorder的問題,當線程1在完成賦值給point過程卻被掛起,線程2開始執行這個代碼的時候檢查第一個point == nullptr?其會發現point != nullptr,所以其會返回一個未經構造函數構造的對象內存。
四、C++11的跨平臺雙檢查鎖的線程安全,使用的是atomic(屏蔽編譯器的reorder);
修改後的代碼以下:this

#ifndef SINGLETON_H
#define SINGLETON_H
#include<iostream>
#include<thread>
#include<mutex>
#include<atomic>
using std::mutex;
using std::lock_guard;
class singleton
{
private:
    singleton(int data=0):data(data)
    { std::cout << "建立構造函數" << std::endl; };
    singleton(const singleton&) {};
    ~singleton() {};
    static singleton* point;
    int data;
public:
    static singleton* get_singleton(int);
    void set_singleton(int);
    void get_data();
};
static mutex mut;
static std::atomic<singleton*> s_p;
singleton* singleton::point= nullptr;
singleton* singleton::get_singleton(int data = 0)
{
    singleton* point = s_p.load(std::memory_order_relaxed);
    std::_Atomic_thread_fence(std::memory_order_acquire);
    if (point== nullptr)
    {
        lock_guard<mutex> lock(mut);//上鎖與自動解鎖
        if(point ==nullptr)
            point = new singleton(data);
        std::_Atomic_thread_fence(std::memory_order_release);
        s_p.store(point, std::memory_order_relaxed);
    }
    return point;
};
void singleton::set_singleton(int data) 
{
    this->data = data;
}
void singleton::get_data() 
{
    std::cout << "the data is : " << this->data<<std::endl;
}
#endif // !SINGLETON_H

固然在JAVA中存在volatile,而c++中沒有。
五、懶漢模式與餓漢模式
上述的都是懶漢模式,即要到使用了才建立對象,還有一種餓漢模式,其是在使用該單一對象前就建立了這個對象,以下所示:atom

class singleton
{
protected:
    singleton()
    {};
private:
    static singleton* p;
public:
    static singleton* initance();
};
singleton* singleton::p = new singleton();
singleton* singleton::initance()
{
    return p;
}

這個是絕對的線程安全的,實際上對於開發者而言餓漢模式恰巧是最懶的,可是存在的問題是,若是這個單一類須要入口參數,那該怎麼辦?線程

3、要點總結

一、Singleton模式中的實例構造器能夠設置爲protected以容許子類派生;二、Singleton模式通常不要支持拷貝構造函數和Colne接口,由於這有可能致使多個對象實例,與Singleton模式的初衷違背;三、如何實現多線程下的Siglenton?注意對雙檢查鎖的正確實現;四、懶漢模式與餓漢模式的區別。

相關文章
相關標籤/搜索