目录
一、单例类简介
单例模式就是让整个程序中仅有该类的一个实例存在。
在很多情况下,只有一个实例是很重要的,比如一个打印机可以有很多打印任务,但是只能有一个任务正在被执行;一个系统只能有一个窗口管理器和文件系统。
从具体实现上来讲,单例类具有如下三个特点: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