所谓单例模式,就是设计一个类,整个程序中只有该类的一个实例存在
单例模式作为最常用的设计模式之一,保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
单例特点:
1 、在任何情况下,单例类永远只有一个实例存在。
2 、单例需要有能力为整个系统提供这一唯一实例。
C++实现单例模式 一般是将构造函数,拷贝构造函数 ,赋值运算符函数声明为私有的,从而禁止他人创建实例。否则如果上面三者不为私有,那么他人就可以调用上面的三个函数来创建实例,我们可以提供一个public的静态方法来获得这个类唯一的一个实例化对象。
单例模式一般存在两种实现模式
即迫不及待,像一个饿汉一样,不管需不需要用到实例都要去创建实例,即在类产生的时候就创建好实例(实例的初始化放在getinstance函数外部,getinstance函数仅返回该唯一实例的指针)。这是一种空间换时间的做法。
即非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化(实例的初始化放在 getinstance函数(getinstance只是一个函数名) 内部)经典的线程安全懒汉模式,使用双检测锁模式(p == NULL检测了两次),利用局部静态变量实现线程安全懒汉模式。这是一种时间换空间的做法,这体现了“懒汉的本性”。
我们先来看看 饿汉模式:
饿汉模式的对象在类产生时候就创建了,一直到程序结束才会去释放。即作为一个单例类实例,它的生存周期和我们的程序一样长。因此该实例对象需要存储在全局数据区,所以肯定需要使用static来修饰,因为类内部的static成员是不属于每个对象的,而是属于整个类的。在加载类的时候,我们的实例对象就产生了。所以对于饿汉模式而言,是线程安全的,因为在线程创建之前实例已经被创建好了。
我们模拟一个饿汉模式的单例类
#include
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;
}
int main(){
cout << "Now we get the instance" << endl;
Singleton* instance1 = Singleton::getInstance();
Singleton* instance2 = Singleton::getInstance();
Singleton* instance3 = Singleton::getInstance();
cout << "Now we destroy the instance" << endl;
return 0;
}
运行结果:
由此可见,对于我们的程序,在我们输出Now we get the instance这句话的时候,也就是我们即将获取到这个类对象的实例的时候,在这之前这个单例类的实例在加载类的时候已经被创建好了,且我们调用了三个getInstance来获取实例,也并没有因此而多创建更多的实例,因此它是一个单例类。在程序结束的时候,这个唯一的实例才会被销毁。
上面我们在singleton.hpp 中实现的实例是一个类对象,现在我们看看来把它换成类对象的指针会怎么样
#pragma once
#include
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 = new Singleton();
Singleton* Singleton::getInstance(){
return instance; //这里就是直接返回instance了而不是返回&instance
}
我们可以发现,没有调用析构函数,没有释放资源,将会导致内存泄漏??!!!!
这是为什么呢?因此此时全局数据区中,存储的并不是一个实例对象,而是一个实例对象的指针,它是一个地址而已,我们真正占有资源的实例对象是存储在堆中的。这样的声明方法可以减小全局数据区的占用量,把一大堆单例对象放在了堆中,但我们需要主动地去调用delete释放申请的资源。我们想要手动调用delete 直接释放该实例是不可能的,因为它的析构函数是私有的,调不到析构函数(析构函数是私有也是我们要求的)。
#include
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();
static void deleteInstance(); //用来销毁实例
};
//下面这个静态成员变量在类加载的时候就已经初始化好了
Singleton* Singleton::instance = new Singleton();
Singleton* Singleton::getInstance(){
return instance;
}
void Singleton::deleteInstance(){
delete instance;
}
int main(){
cout << "Now we get the instance" << endl;
Singleton* instance1 = Singleton::getInstance();
Singleton* instance2 = Singleton::getInstance();
Singleton* instance3 = Singleton::getInstance();
cout << "Now we destroy the instance" << endl;
Singleton::deleteInstance();
return 0;
}
但是这样的写法,常常会忘记手动去调用函数来释放资源,于是,我们想到了,可不可以有一个自动地能够释放资源的函数。
#include
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 = new Singleton();
Singleton::Garbo Singleton::_garbo; //还需要初始化一个垃圾清理的静态成员变量
Singleton* Singleton::getInstance(){
return instance;
}
int main(){
cout << "Now we get the instance" << endl;
Singleton* instance1 = Singleton::getInstance();
Singleton* instance2 = Singleton::getInstance();
Singleton* instance3 = Singleton::getInstance();
cout << "Now we destroy the instance" << endl;
return 0;
}
我们成功地销毁了对象,而且还没有手动去释放!Perfect!当然了,我们想到,为什么不尝试用一用智能指针呢?智能指针不就是为了能够让我们不需要手动释放资源而设计的么,它会自动去释放资源啊?现在我们尝试着用智能指针试试创建一个单例类。
#include
#include
using namespace std;
class Singleton{
private:
Singleton(){
cout << "创建了一个单例对象" << endl;
}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton(){
//析构函数我们也需要声明成private的
//因为我们想要这个实例在程序运行的整个过程中都存在
//所以我们不允许实例自己主动调用析构函数释放对象
cout << "销毁了一个单例对象" << endl;
}
static shared_ptr<Singleton> instance; //这是我们的单例对象,它是一个类对象的指针
public:
static shared_ptr<Singleton> getInstance();
};
//下面这个静态成员变量在类加载的时候就已经初始化好了
shared_ptr<Singleton> Singleton::instance(new Singleton());
shared_ptr<Singleton> Singleton::getInstance(){
return instance;
}
int main(){
cout << "Now we get the instance" << endl;
shared_ptr<Singleton> instance1 = Singleton::getInstance();
shared_ptr<Singleton> instance2 = Singleton::getInstance();
shared_ptr<Singleton> instance3 = Singleton::getInstance();
cout << "Now we destroy the instance" << endl;
return 0;
}
你会发现便错误
原因是shared_ptr无法访问私有化的析构函数。但是我们又需要析构函数是私有的,这就矛盾起来了(为什么希望析构函数是私有的如上注释)。因此,我们就需要用到shared_ptr可以指定删除器的特点,自定义删除器。这一点是我之前不知道的,记录一下啊
#include
#include
using namespace std;
class Singleton{
private:
Singleton(){
cout << "创建了一个单例对象" << endl;
}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton(){
//析构函数我们也需要声明成private的
//因为我们想要这个实例在程序运行的整个过程中都存在
//所以我们不允许实例自己主动调用析构函数释放对象
cout << "销毁了一个单例对象" << endl;
}
static void DestroyInstance(Singleton*); //自定义一个释放实例的函数
static shared_ptr<Singleton> instance; //这是我们的单例对象,它是一个类对象的指针
public:
static shared_ptr<Singleton> getInstance();
};
//下面这个静态成员变量在类加载的时候就已经初始化好了
shared_ptr<Singleton> Singleton::instance(new Singleton(),Singleton::DestroyInstance);
shared_ptr<Singleton> Singleton::getInstance(){
return instance;
}
void Singleton::DestroyInstance(Singleton*){
cout << "在自定义函数中释放实例" << endl;
}
int main(){
cout << "Now we get the instance" << endl;
shared_ptr<Singleton> instance1 = Singleton::getInstance();
shared_ptr<Singleton> instance2 = Singleton::getInstance();
shared_ptr<Singleton> instance3 = Singleton::getInstance();
cout << "Now we destroy the instance" << endl;
return 0;
}
好了,饿汉模式的单例类讲完了,因为单例模式在程序一开始就初始化好实例,所以后续不再需要考虑线程安全的问题,因此它适用于线程比较多的程序中,以空间换取时间,提高了效率。
但在懒汉模式中,情况就不一样了,因为它是在使用时才创建实例,在第一次调用getInstance()的时候,才创建实例对象。如果有多个线程,同时调用了getInstance()获取实例,那么可能就会产生多个实例,那么这就不是我们的单例模式了。因此我们需要做一点事情,才能够避免这种情况的发生
分割线-----------------------------------------------------------
所谓懒汉模式,就是像一个懒汉一样,需要用到创建实例了程序再去创建实例,不需要创建实例程序就“懒得”去创建实例,这是一种时间换空间的做法,这体现了“懒汉的本性”。
经典的线程安全懒汉模式,使用双检测锁模式(p == NULL检测了两次)
利用局部静态变量实现线程安全懒汉模式
如果有多个线程,同时调用了获取实例,那么可能就会产生多个实例,那么这就不是我们的单例模式了。所以我们需要做一些事情才能保证我们是一个单例类。当然,我们先来考虑是单线程的情况,再去考虑多线程的情况,我们总得先把懒汉模式的本质给弄明白。
对于饿汉模式来说,它在单例类内部定义了一个类对象的指针,在初始化这个指针的时候,直接调用new在堆上申请了空间。于是达到了这种效果,在进入main函数之前,这个单例类的实例已经创建好了。
而对于懒汉模式而言,我们可不可以不让这个类对象指针在初始化的时候就new,而是给它赋一个NULL,那这样,在进入main函数之前,这个类对象指针只是一个空指针,并没有产生实际的对象。而当我们在程序中调用getInstance函数时,就需要进行一个判断,如果该类对象指针为空,那么我们就需要调用new创建一个对象,而如果该类对象指针不为空,那么我们就不用创建对象直接返回该指针就好了。根据这个思路,我们实现了下面的代码(因为我们的静态成员变量采用的是类对象的指针而不是类对象,因此我们需要写一个垃圾回收机制,这里我们采用的是上一篇文章的方法二,即实现一个内部的垃圾回收类)
#include
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;
}
int main(){
cout << "Now we get the instance" << endl;
Singleton* instance1 = Singleton::getInstance();
Singleton* instance2 = Singleton::getInstance();
Singleton* instance3 = Singleton::getInstance();
cout << "Now we destroy the instance" << endl;
return 0;
}
可以看到我们先进入了main函数中,在调用了第一个getInstance获取实例的时候才创建了一个单例对象。在调用后续的getInstance获取实例的时候因为instance不为空,所以直接返回instance。
当然了上面我们是在堆上创建了对象,想在栈上创建对象也是可以的
#include
using namespace std;
class Singleton{
private:
Singleton(){
cout << "创建了一个单例对象" << endl;
}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton(){
// 析构函数我们也需要声明成private的
//因为我们想要这个实例在程序运行的整个过程中都存在
//所以我们不允许实例自己主动调用析构函数释放对象
cout << "销毁了一个单例对象" << endl;
}
public:
static Singleton* getInstance();
};
Singleton* Singleton::getInstance(){
static Singleton instance;
return &instance;
}
int main(){
cout << "Now we get the instance" << endl;
Singleton* instance1 = Singleton::getInstance();
Singleton* instance2 = Singleton::getInstance();
Singleton* instance3 = Singleton::getInstance();
cout << "Now we destroy the instance" << endl;
return 0;
}
上面这两种情况在单线程的情况下,是没有问题。但是试想在多线程的情况下,试想一下如果多个线程同时调用了getInstance函数,此时可能存在多线程竞态环境,就可能会产生重复构造或者是构造不完全等问题。因此我们在getInstance函数中还要采用线程同步的方式,保证线程同步,从而达到单例类的目的
在堆上创建对象
#include
#include
#include
pthread_mutex_t m_mutex;
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(){
pthread_mutex_lock(&m_mutex); // 加锁
if(instance == NULL)
instance = new Singleton();
pthread_mutex_unlock(&m_mutex); // 解锁
return instance;
}
int main(){
cout << "Now we get the instance" << endl;
Singleton* instance1 = Singleton::getInstance();
Singleton* instance2 = Singleton::getInstance();
Singleton* instance3 = Singleton::getInstance();
cout << "Now we destroy the instance" << endl;
return 0;
}
在栈上创建对象
#include
#include
#include
pthread_mutex_t m_mutex;
using namespace std;
class Singleton{
private:
Singleton(){
cout << "创建了一个单例对象" << endl;
}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton(){
// 析构函数我们也需要声明成private的
//因为我们想要这个实例在程序运行的整个过程中都存在
//所以我们不允许实例自己主动调用析构函数释放对象
cout << "销毁了一个单例对象" << endl;
}
public:
static Singleton* getInstance();
};
Singleton* Singleton::getInstance(){
pthread_mutex_lock(&m_mutex); //加锁
static Singleton instance;
pthread_mutex_unlock(&m_mutex); //解锁
return &instance;
}
int main(){
cout << "Now we get the instance" << endl;
Singleton* instance1 = Singleton::getInstance();
Singleton* instance2 = Singleton::getInstance();
Singleton* instance3 = Singleton::getInstance();
cout << "Now we destroy the instance" << endl;
return 0;
}
Lock()和UnLock()原作者没有实现,这是一个加锁的步骤而已,都是常见的线程同步的方法。我加入了pthread_mutex_unlock()的方法
当两个线程同时想创建一个实例,由于在一个时刻只有一个线程能得到同步锁,当一个线程加上锁时,第二个线程只能等待,当第一个线程发现实例还没有创建时,它创建出第一个实例。接着第一个线程释放同步锁,此时第二个线程可以加上同步锁,这个时候由于实例已经被第一个线程创建出来了,第二个线程就不会重复创建了,这样就保证了我们在多线程环境中也只能得到一个实例。
但是问题就出来了,加锁和解锁的操作是需要时间的,因此在线程很多的情况下,就会浪费大量的时间,导致效率下降。下面我们可以用一种优化的方法(因为只改动了getInstance这个函数,于是我只把这个函数写了出来)。
Singleton* Singleton::getInstance(){
if(instance == NULL){
Lock(); // 加锁
if(instance == NULL)
instance = new Singleton();
UnLock(); // 解锁
}
return instance;
}
上面这种方法只有在instance为空的时候才需要加锁和解锁操作,如果已经创建出来了实例,则无需加锁。所以上面的代码只有在第一个创建实例时才会需要加锁。这样的方法效率已经很好了,只是实现起来比较麻烦,容易出错。
4月12日补充---------------
在另一篇博客看到,双检锁的机制(DCL: Double-Checked Locking Pattern)还是有一些问题的。
关于memory model。在某些内存模型中(虽然不常见)或者是由于编译器的优化以及运行时优化等等原因,使得instance虽然已经不是nullptr但是其所指对象还没有完成构造,这种情况下,另一个线程如果调用getInstance()就有可能使用到一个不完全初始化的对象。换句话说,就是代码中第2行:if(instance == NULL)和第六行instance = new Singleton();没有正确的同步,在某种情况下会出现new返回了地址赋值给instance变量而Singleton此时还没有构造完全,当另一个线程随后运行到第2行时将不会进入if从而返回了不完全的实例对象给用户使用,造成了严重的错误。在C++11没有出来的时候,只能靠插入两个memory barrier(内存屏障)来解决这个错误,但是C++11引进了memory model,提供了Atomic实现内存的同步访问,即不同线程总是获取对象修改前或修改后的值,无法在对象修改期间获得该对象。
static Singleton* getInstance() {
if(instance == NULL) {
Lock lock; // 基于作用域的加锁,超出作用域,自动调用析构函数解锁
if(instance == NULL) {
instance = new Singleton();
}
}
return instance;
}
C++11规定了local static在多线程条件下的初始化行为,要求编译器保证了内部静态变量的线程安全性。在C++11标准下,《Effective C++》提出了一种更优雅的单例模式实现,使用函数内的 local static 对象。这样,只有当第一次访问getInstance()方法时才创建实例。这种方法也被称为Meyers’ Singleton。C++0x之后该实现是线程安全的,C++0x之前仍需加锁。
class Singleton
{
private:
Singleton() { };
~Singleton() { };
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
public:
static Singleton& getInstance()
{
static Singleton instance;
return instance;
}
};
懒汉模式适用于线程比较少的场景,因为线程一旦多,加锁的开销就会体现出来(当然最后对懒汉优化的那种方案已经差不多解决这个问题了),总之它是一种时间换空间的模式。
饿汉模式适用于线程比较多的场景,它会占用全局静态区一定的空间,但是能够确保只有一个实例。但是当线程很少,甚至是没有用到这个单例类的时候,就显得得不偿失了,它占用的空间问题就体现出来了,这是一种空间换时间的模式。
以上我参考了
单例模式——饿汉模式
单例模式——懒汉模式