設(shè)計(jì)模式之單例模式

一、什么是單例模式

屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。這種模式涉及到一個(gè)單一的類,該類負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建。這個(gè)類提供了一種訪問(wèn)其唯一的對(duì)象的方式,可以直接訪問(wèn),不需要實(shí)例化該類的對(duì)象。

注意:

  • 單例類只能有一個(gè)實(shí)例。
  • 單例類必須自己創(chuàng)建自己的唯一實(shí)例。
  • 單例類必須給所有其他對(duì)象提供這一實(shí)例。

二、適用場(chǎng)景和實(shí)現(xiàn)要點(diǎn)

1、適用場(chǎng)景

  • 需要生成唯一序列的環(huán)境(頻繁訪問(wèn)數(shù)據(jù)庫(kù)或文件)。
  • 需要頻繁實(shí)例化然后銷毀的對(duì)象。
  • 創(chuàng)建對(duì)象時(shí)耗時(shí)過(guò)多或者耗資源過(guò)多,但又經(jīng)常用到的對(duì)象。
  • 方便資源相互通信的環(huán)境
1.1 單例模式經(jīng)典使用場(chǎng)景
  1. 資源共享的情況下,避免由于資源操作時(shí)導(dǎo)致的性能或損耗等。如上述中的日志文件,應(yīng)用配置。
  2. 控制資源的情況下,方便資源之間的互相通信。如線程池等。
1.2 應(yīng)用場(chǎng)景舉例
  1. 外部資源:每臺(tái)計(jì)算機(jī)有若干個(gè)打印機(jī),但只能有一個(gè)PrinterSpooler,以避免兩個(gè)打印作業(yè)同時(shí)輸出到打印機(jī)。內(nèi)部資源:大多數(shù)軟件都有一個(gè)(或多個(gè))屬性文件存放系統(tǒng)配置,這樣的系統(tǒng)應(yīng)該有一個(gè)對(duì)象管理這些屬性文件 。
  2. Windows的Task Manager(任務(wù)管理器)就是很典型的單例模式。
  3. windows的Recycle Bin(回收站)也是典型的單例應(yīng)用。在整個(gè)系統(tǒng)運(yùn)行過(guò)程中,回收站一直維護(hù)著僅有的一個(gè)實(shí)例。
  4. 網(wǎng)站的計(jì)數(shù)器,一般也是采用單例模式實(shí)現(xiàn),否則難以同步。
  5. 應(yīng)用程序的日志應(yīng)用,一般都何用單例模式實(shí)現(xiàn),這一般是由于共享的日志文件一直處于打開(kāi)狀態(tài),因?yàn)橹荒苡幸粋€(gè)實(shí)例去操作,否則內(nèi)容不好追加。
  6. Web應(yīng)用的配置對(duì)象的讀取,一般也應(yīng)用單例模式,這個(gè)是由于配置文件是共享的資源。
  7. 數(shù)據(jù)庫(kù)連接池的設(shè)計(jì)一般也是采用單例模式,因?yàn)閿?shù)據(jù)庫(kù)連接是一種數(shù)據(jù)庫(kù)資源。數(shù)據(jù)庫(kù)軟件系統(tǒng)中使用數(shù)據(jù)庫(kù)連接池,主要是節(jié)省打開(kāi)或者關(guān)閉數(shù)據(jù)庫(kù)連接所引起的效率損耗,這種效率上的損耗還是非常昂貴的,因?yàn)楹斡脝卫J絹?lái)維護(hù),就可以大大降低這種損耗。
  8. 多線程的線程池的設(shè)計(jì)一般也是采用單例模式,這是由于線程池要方便對(duì)池中的線程進(jìn)行控制。
  9. 操作系統(tǒng)的文件系統(tǒng),也是大的單例模式實(shí)現(xiàn)的具體例子,一個(gè)操作系統(tǒng)只能有一個(gè)文件系統(tǒng)。
  10. HttpApplication 也是單位例的典型應(yīng)用。熟悉ASP.Net(IIS)的整個(gè)請(qǐng)求生命周期的人應(yīng)該知道HttpApplication也是單例模式,所有的HttpModule都共享一個(gè)HttpApplication實(shí)例.

2、實(shí)現(xiàn)要點(diǎn)

  • 將該類的\color{rgb(255,0,0)}{構(gòu)造方法定義為私有方法},這樣其他處的代碼就無(wú)法通過(guò)調(diào)用該類的構(gòu)造方法來(lái)實(shí)例化該類的對(duì)象,只有通過(guò)該類提供的靜態(tài)方法來(lái)得到該類的唯一實(shí)例;
  • \color{rgb(255,0,0)}{禁止賦值和拷貝}
  • 在該類內(nèi)提供一個(gè)\color{rgb(255,0,0)}{靜態(tài)方法},當(dāng)我們調(diào)用這個(gè)方法時(shí),如果類持有的引用不為空就返回這個(gè)引用,如果類保持的引用為空就創(chuàng)建該類的實(shí)例并將實(shí)例的引用賦予該類保持的引用;
  • 提供一個(gè)靜態(tài)的公有的函數(shù)用于創(chuàng)建或獲取它本身的靜態(tài)私有對(duì)象。

三、C++實(shí)現(xiàn)單例的幾種方式

1、有缺陷的懶漢式

懶漢式(Lazy-Initialization)的方法是\color{rgb(255,0,0)}{直到使用時(shí)才實(shí)例化對(duì)象},也就說(shuō)直到調(diào)用getInstance() 方法的時(shí)候才 new 一個(gè)單例的對(duì)象, 如果不被調(diào)用就不會(huì)占用內(nèi)存。

// 有如下問(wèn)題:
// 1.線程不安全
// 2.內(nèi)存泄漏
class Singleton
{
private:
    Singleton()
    {
        std::cout<<"創(chuàng)建構(gòu)造函數(shù)!"<<std::endl;
    }
    Singleton(Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
    static Singleton* m_pInstance;

public:
    ~Singleton()
    {
        std::cout<<"析構(gòu)函數(shù)!"<<std::endl;
    }

    static Singleton* getInstance()
    {
        if (m_pInstance == nullptr)
        {
            m_pInstance = new Singleton;
        }
        return m_pInstance ;
    }
};

Singleton* Singleton::m_instance_ptr = nullptr;

int main()
{
    Singleton* instance = Singleton::getInstance();
    Singleton* instance_2 = Singleton::getInstance();
    return 0;
}

運(yùn)行結(jié)果是

創(chuàng)建構(gòu)造函數(shù)!

可以看到,獲取了兩次類的實(shí)例,卻只有一次類的構(gòu)造函數(shù)被調(diào)用,表明只生成了唯一實(shí)例,這是個(gè)最基礎(chǔ)版本的單例實(shí)現(xiàn),同時(shí)也存在一些問(wèn)題

  1. 線程安全問(wèn)題,當(dāng)多線程獲取單例時(shí)有可能引發(fā)競(jìng)態(tài)條件:
    第一個(gè)線程在if中判斷m_pInstance 為空,實(shí)例化單例對(duì)象;同時(shí)第二個(gè)線程在獲取單例時(shí)也判斷m_pInstance 為空,實(shí)例化單例對(duì)象,會(huì)實(shí)例化兩個(gè)對(duì)象。
  2. 內(nèi)存泄漏,注意到類中只負(fù)責(zé)new出對(duì)象,卻沒(méi)有負(fù)責(zé)delete對(duì)象,因此只有構(gòu)造函數(shù)被調(diào)用,析構(gòu)函數(shù)卻沒(méi)有被調(diào)用;因此會(huì)導(dǎo)致內(nèi)存泄漏。

2、線程安全且內(nèi)存安全的懶漢式單例 (智能指針,鎖)

class Singleton
{
public:
    typedef std::shared_ptr<Singleton> Ptr;
    ~Singleton()
    {
        std::cout<<"析構(gòu)函數(shù)!"<<std::endl;
    }
    Singleton(Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;

    static Ptr getInstance()
    {
        // 雙檢鎖
        if (m_pInstance == nullptr)
        {
            std::lock_guard<std::mutex> lock(m_mutex);
            if (m_pInstance == nullptr)
            {
                m_pInstance  = std::shared_ptr<Singleton>(new Singleton);
            }
        }
        return m_pInstance ;
    }

private:
    Singleton()
    {
        std::cout<<"創(chuàng)建構(gòu)造函數(shù)!"<<std::endl;
    }
    
    static Ptr m_pInstance;
    static std::mutex m_mutex;
};

Singleton::Ptr Singleton::m_instance_ptr = nullptr;
std::mutex Singleton::m_mutex;

int main()
{
    Singleton* instance = Singleton::getInstance();
    Singleton* instance_2 = Singleton::getInstance();
    return 0;
}

運(yùn)行結(jié)果如下,發(fā)現(xiàn)確實(shí)只構(gòu)造了一次實(shí)例,并且發(fā)生了析構(gòu)。

創(chuàng)建構(gòu)造函數(shù)!
析構(gòu)函數(shù)!

shared_ptr和mutex都是C++11的標(biāo)準(zhǔn),以上這種方法的優(yōu)點(diǎn)是

  • 基于 shared_ptr, 用了C++比較倡導(dǎo)的 RAII思想,用對(duì)象管理資源,當(dāng) shared_ptr 析構(gòu)的時(shí)候,new 出來(lái)的對(duì)象也會(huì)被 delete掉。以此避免內(nèi)存泄漏。
  • 加了鎖,使用互斥量來(lái)達(dá)到線程安全。這里使用了兩個(gè) if判斷語(yǔ)句的技術(shù)稱為雙檢鎖;好處是,只有判斷指針為空的時(shí)候才加鎖,避免每次調(diào)用 getInstance的方法都加鎖,鎖的開(kāi)銷畢竟還是有點(diǎn)大的。

不足之處在于: 使用智能指針會(huì)要求用戶也得使用智能指針,非必要不應(yīng)該提出這種約束; 使用鎖也有開(kāi)銷; 同時(shí)代碼量也增多了,實(shí)現(xiàn)上我們希望越簡(jiǎn)單越好。還有更為嚴(yán)重的問(wèn)題,在某些平臺(tái)(與編譯器和指令集架構(gòu)有關(guān)),雙檢鎖會(huì)失效!

3、最推薦的懶漢式單例(magic static )——局部靜態(tài)變量

class Singleton
{
public:
    ~Singleton()
    {
        std::cout<<"析構(gòu)函數(shù)!"<<std::endl;
    }
    Singleton(Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
    static Singleton& getInstance()
    {
        static Singleton instance;
        return instance;
    }

private:
    Singleton()
    {
        std::cout<<"創(chuàng)建構(gòu)造函數(shù)!"<<std::endl;
    }
};

int main()
{
    Singleton& instance = Singleton::getInstance();
    Singleton& instance_2 = Singleton::getInstance();
    return 0;
}

運(yùn)行結(jié)果

創(chuàng)建構(gòu)造函數(shù)!
析構(gòu)函數(shù)!

運(yùn)用C++靜態(tài)變量的特性,生存周期是從聲明到程序結(jié)束。

四、單例模式優(yōu)缺點(diǎn)

優(yōu)點(diǎn)

  1. 在單例模式中,活動(dòng)的單例只有一個(gè)實(shí)例,對(duì)單例類的所有實(shí)例化得到的都是相同的一個(gè)實(shí)例。這樣就防止其它對(duì)象對(duì)自己的實(shí)例化,確保所有的對(duì)象都訪問(wèn)一個(gè)實(shí)例
  2. 單例模式具有一定的伸縮性,類自己來(lái)控制實(shí)例化進(jìn)程,類就在改變實(shí)例化進(jìn)程上有相應(yīng)的伸縮性。
  3. 提供了對(duì)唯一實(shí)例的受控訪問(wèn)。
  4. 由于在系統(tǒng)內(nèi)存中只存在一個(gè)對(duì)象,因此可以 節(jié)約系統(tǒng)資源,當(dāng) 需要頻繁創(chuàng)建和銷毀的對(duì)象時(shí)單例模式無(wú)疑可以提高系統(tǒng)的性能。
  5. 允許可變數(shù)目的實(shí)例。
  6. 避免對(duì)共享資源的多重占用。

缺點(diǎn)

  1. 不適用于變化的對(duì)象,如果同一類型的對(duì)象總是要在不同的用例場(chǎng)景發(fā)生變化,單例就會(huì)引起數(shù)據(jù)的錯(cuò)誤,不能保存彼此的狀態(tài)。
  2. 由于單利模式中沒(méi)有抽象層,因此單例類的擴(kuò)展有很大的困難。
  3. 單例類的職責(zé)過(guò)重,在一定程度上違背了“單一職責(zé)原則”。
  4. 濫用單例將帶來(lái)一些負(fù)面問(wèn)題,如為了節(jié)省資源將數(shù)據(jù)庫(kù)連接池對(duì)象設(shè)計(jì)為的單例類,可能會(huì)導(dǎo)致共享連接池對(duì)象的程序過(guò)多而出現(xiàn)連接池溢出;如果實(shí)例化的對(duì)象長(zhǎng)時(shí)間不被利用,系統(tǒng)會(huì)認(rèn)為是垃圾而被回收,這將導(dǎo)致對(duì)象狀態(tài)的丟失。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容