单例设计模式及单例类的多线程保护问题

目录

一、单例类简介

二、单例类的实现模式

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

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