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

mac2025-03-02  3

目录

一、单例类简介

二、单例类的实现模式

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

最新回复(0)