單例設計模式及單例類的多線程保護問題

目錄

一、單例類簡介

二、單例類的實現模式

2.1餓漢模式代碼

2.2懶漢模式代碼

2.3多線程保護的懶漢模式


一、單例類簡介

單例模式就是讓整個程序中僅有該類的一個實例存在。

在很多情況下,只有一個實例是很重要的,比如一個打印機可以有很多打印任務,但是隻能有一個任務正在被執行;一個系統只能有一個窗口管理器和文件系統。

從具體實現上來講,單例類具有如下三個特點:1)構造函數、拷貝構造函數、操作符重載的賦值運算符函數應位於private權限修飾符下,若不如此的話,他人可以在類外調用這三個函數創建實例,那麼就無法實現單例模式。2)類的成員變量中包含一個該類的靜態私有對象,或一個指向該類對象的靜態私有指針。3)該類提供了一個靜態的公有函數,用於創建或獲取2)中的靜態私有對象或指針。

二、單例類的實現模式

單例類的實現模式有兩種:餓漢模式和懶漢模式。

餓漢模式:如同一個餓漢一樣,在進入main函數之前就創建好了實例,不管目前需不需要用到,這是一種空間換時間的做法。不需要進行線程保護。

懶漢模式:如同一個懶漢一樣,需要創建實例的時候纔去創建,否則就不創建,是一種時間換空間的做法。需要進行線程保護。

2.1餓漢模式代碼

餓漢模式中的單例類對象instance爲該單例類Singleton內的一個私有static成員(該成員也爲此單例類Singleton的對象),在加載類時便創建了,直到程序結束時才釋放,即餓漢模式下的單例類對象的生存週期與程序一樣長,因此,餓漢模式是線程安全的,因爲在線程創建之前,實例就已經被創建好了。

singleton.hpp

#pragma once
 
#include <iostream>
using namespace std;
 
class Singleton{
private:
    Singleton(){
        cout << "創建了一個單例對象" << endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        //析構函數我們也需要聲明成private的
        //因爲我們想要這個實例在程序運行的整個過程中都存在
        //所以我們不允許實例自己主動調用析構函數釋放對象
        cout << "銷燬了一個單例對象" << endl;
    }
 
 
    static Singleton instance;  //這是我們的單例對象,注意這是一個類對象,下面會更改這個類型
public:
    static Singleton* getInstance();
};
 
//下面這個靜態成員變量在類加載的時候就已經初始化好了
Singleton Singleton::instance; 
 
Singleton* Singleton::getInstance(){
    return &instance;
}

test.cpp

#include "singleton.hpp"
 
int main(){
   cout << "Now we get the instance" << endl;
    Singleton* instance1 = Singleton::getInstance();
    Singleton* instance2 = Singleton::getInstance();
    Singleton* instance3 = Singleton::getInstance();
    cout<<"instance1:"<<instance1<<endl;
    cout<<"instance2:"<<instance2<<endl;
    cout<<"instance3:"<<instance3<<endl;
    cout << "Now we destroy the instance" << endl;
    return 0;;
}

最後的運行結果是:

由此可見,程序在進入main函數之前就已經創建好了一個單例對象instance,進入main函數後三次調用getInstance函數獲得的都是單例對象instance的地址,因此instance1、instance2和instance3中的值是一樣的,並沒有創建更多的實例。最後程序結束時調用析構函數銷燬了該單例對象。

2.2懶漢模式代碼

與餓漢模式不同,懶漢模式在需要時才創建實例,是一種以時間換空間的做法。在單線程情況下,它不需要進行線程保護,代碼如下。

#pragma once
 
#include <iostream>
using namespace std;
 
class Singleton{
private:
    Singleton(){
        cout << "創建了一個單例對象" << endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        // 析構函數我們也需要聲明成private的
        //因爲我們想要這個實例在程序運行的整個過程中都存在
        //所以我們不允許實例自己主動調用析構函數釋放對象
        cout << "銷燬了一個單例對象" << endl;                                            
    }
         
    static Singleton* instance;  //這是我們的單例對象,它是一個類對象的指針
public:
    static Singleton* getInstance();
                                        
private:
    //定義一個內部類
    class Garbo{
    public:
        Garbo(){}
        ~Garbo(){
            if(instance != NULL){
                delete instance;
                instance = NULL;
            }
        }
    };
 
    //定義一個內部類的靜態對象
    //當該對象銷燬的時候,調用析構函數順便銷燬我們的單例對象
    static Garbo _garbo;
};
 
//下面這個靜態成員變量在類加載的時候就已經初始化好了
Singleton* Singleton::instance = NULL; 
Singleton::Garbo Singleton::_garbo;     //還需要初始化一個垃圾清理的靜態成員變量
 
Singleton* Singleton::getInstance(){
    if(instance == NULL)
        instance = new Singleton();
    return instance;   
}

test.cpp

#include "singleton.hpp"
 
int main(){
    cout << "Now we get the instance" << endl;
    Singleton* instance1 = Singleton::getInstance();
    Singleton* instance2 = Singleton::getInstance();
    Singleton* instance3 = Singleton::getInstance();
    cout<<"instance1:"<<instance1<<endl;
    cout<<"instance2:"<<instance2<<endl;
    cout<<"instance3:"<<instance3<<endl;
    cout << "Now we destroy the instance" << endl;
    return 0;
}

運行結果如下所示。

懶漢模式下的單例instance被初始化爲NULL,在調用getInstance函數獲取單例地址時,首先判斷instance是否爲NULL,如果爲NULL,代表單例還未被創建,使用new在堆上創建單例instance,並返回該instance的地址,下次再調用getInstance獲取單例地址時,因爲instance已經不爲NULL了,所以直接將上次在堆上創建的單例instance的地址返回。

另外,類中類Garbo是一個垃圾回收類,作用是在程序結束時釋放堆上的單例instance,原理是:在程序結束時會調用~Garbo()析構靜態成員變量_garbo,而在~Garbo()中,若判斷instance不爲空(即單例被創建了),則調用delete釋放堆上的單例,否則不做任何處理。

2.3多線程保護的懶漢模式

在上面的單線程情況下,懶漢模式是沒有問題的,可是在多線程情況下,它就需要進行線程保護!

爲什麼呢?假設線程1和線程2均調用了getInstance函數獲取單例。在線程1剛判斷完instance爲NULL,正準備調用new在堆上創建單例時,上下文切換到了線程2,此時線程2也認爲instance爲NULL,於是調用new創建了單例instance,然後又上下文切換到線程1,線程1接着之前的步驟執行,即調用new在堆上又創建了一個單例instance。大家肯定發現了吧,現在程序中存在着兩個Singleton對象instance,那麼Singleton對象還能被稱作爲單例類嗎?而這,就是BUG所在。

餓漢的單例模式在多線程情況下進行線程保護的代碼如下:

Singleton.hpp

#pragma once
 
#include <iostream>
using namespace std; 

class Lock  {  
private:         
    mutex my_mutex;  
public:  
    Lock(mutex my_mutex1) : my_mutex(my_mutex1){  
        m_cs.Lock();  
    }
    ~Lock(){  
         m_cs.Unlock();  
    }  
};  

class Singleton{
private:
    Singleton(){
        cout << "創建了一個單例對象" << endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        // 析構函數我們也需要聲明成private的
        //因爲我們想要這個實例在程序運行的整個過程中都存在
        //所以我們不允許實例自己主動調用析構函數釋放對象
        cout << "銷燬了一個單例對象" << endl;                                            
    }
         
    static Singleton* instance;  //這是我們的單例對象,它是一個類對象的指針
public:
    static Singleton* getInstance();
    static mutex my_mutex1;
                                        
private:
    //定義一個內部類
    class Garbo{
    public:
        Garbo(){}
        ~Garbo(){
            if(instance != NULL){
                delete instance;
                instance = NULL;
            }
        }
    };
 
    //定義一個內部類的靜態對象
    //當該對象銷燬的時候,調用析構函數順便銷燬我們的單例對象
    static Garbo _garbo;
};
 
//下面這個靜態成員變量在類加載的時候就已經初始化好了
Singleton* Singleton::instance = NULL; 
Singleton::Garbo Singleton::_garbo;     //還需要初始化一個垃圾清理的靜態成員變量
 
Singleton* Singleton::getInstance(){
    if(instance == NULL){
        Lock lock(my_mutex1); // 加鎖
        if(instance == NULL)
            instance = new Singleton();
    }  
    return instance;
}

其中,getInstance()中的第一個if(instance == NULL)是用來防止在線程很多的情況下,每個線程調用getInstance()時都進行加鎖解鎖,會浪費很多時間。第二個if(instance == NULL)則是用來判斷單例是否被創建了。另外,Lock類對象lock在構造時會執行my_mutex1.lock()加鎖,而在退出getInstance函數時,Lock類對象lock會自動調用析構函數~Lock(),從而執行my_mutex1.unlock()解鎖,實現了對單例類的多線程保護。

最後,推薦三篇比較經典的單例模式博文,同時也是本文所重點參考的。

1、https://blog.csdn.net/lvyibin890/article/details/81943637

2、https://blog.csdn.net/lvyibin890/article/details/81946863

3、https://www.cnblogs.com/qianqiannian/p/6541884.html

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