上一篇文章讲到了单例模式的饿汉模式,这一篇来讨论一下单例模式的懒汉模式。
所谓懒汉模式,就是像一个懒汉一样,需要用到创建实例了程序再去创建实例,不需要创建实例程序就“懒得”去创建实例,这是一种时间换空间的做法,这体现了“懒汉的本性”。
如果有多个线程,同时调用了获取实例,那么可能就会产生多个实例,那么这就不是我们的单例模式了。所以我们需要做一些事情才能保证我们是一个单例类。
额当然,我们先来考虑是单线程的情况,再去考虑多线程的情况,我们总得先把懒汉模式的本质给弄明白。
对于饿汉模式来说,它在单例类内部定义了一个类对象的指针,在初始化这个指针的时候,直接调用new在堆上申请了空间。于是达到了这种效果,在进入main函数之前,这个单例类的实例已经创建好了。
而对于懒汉模式而言,我们可不可以不让这个类对象指针在初始化的时候就new,而是给它赋一个NULL,那这样,在进入main函数之前,这个类对象指针只是一个空指针,并没有产生实际的对象。而当我们在程序中调用getInstance函数时,就需要进行一个判断,如果该类对象指针为空,那么我们就需要调用new创建一个对象,而如果该类对象指针不为空,那么我们就不用创建对象直接返回该指针就好了。根据这个思路,我们实现了下面的代码(因为我们的静态成员变量采用的是类对象的指针而不是类对象,因此我们需要写一个垃圾回收机制,这里我们采用的是上一篇文章的方法二,即实现一个内部的垃圾回收类)
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();
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.cc
#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 << "Now we destroy the instance" << endl;
return 0;
}
程序的运行结果如下图
可以看到我们先进入了main函数中,在调用了第一个getInstance获取实例的时候才创建了一个单例对象。在调用后续的getInstance获取实例的时候因为instance不为空,所以直接返回instance。
当然了上面我们是在堆上创建了对象,想在栈上创建对象也是可以的
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;
}
public:
static Singleton* getInstance();
};
Singleton* Singleton::getInstance(){
static Singleton instance;
return &instance;
}
test.cc
#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 << "Now we destroy the instance" << endl;
return 0;
}
结果是一样的
上面这两种情况在单线程的情况下,是没有问题。但是试想在多线程的情况下,试想一下如果多个线程同时调用了getInstance函数,此时可能存在多线程竞态环境,就可能会产生重复构造或者是构造不完全等问题。因此我们在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();
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(){
Lock(); // 加锁
if(instance == NULL)
instance = new Singleton();
UnLock(); // 解锁
return instance;
}
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;
}
public:
static Singleton* getInstance();
};
Singleton* Singleton::getInstance(){
Lock(); //加锁
static Singleton instance;
UnLock(); //解锁
return &instance;
}
其中,这些Lock()和UnLock()我并没有实现,这是一个加锁的步骤而已,都是常见的线程同步的方法,所以懂就好啦~~~
当两个线程同时想创建一个实例,由于在一个时刻只有一个线程能得到同步锁,当一个线程加上锁时,第二个线程只能等待,当第一个线程发现实例还没有创建时,它创建出第一个实例。接着第一个线程释放同步锁,此时第二个线程可以加上同步锁,这个时候由于实例已经被第一个线程创建出来了,第二个线程就不会重复创建了,这样就保证了我们在多线程环境中也只能得到一个实例。
但是问题就出来了,加锁和解锁的操作是需要时间的,因此在线程很多的情况下,就会浪费大量的时间,导致效率下降。下面我们可以用一种优化的方法(因为只改动了getInstance这个函数,于是我只把这个函数写了出来)。
Singleton* Singleton::getInstance(){
if(instance == NULL){
Lock(); // 加锁
if(instance == NULL)
instance = new Singleton();
UnLock(); // 解锁
}
return instance;
}
上面这种方法只有在instance为空的时候才需要加锁和解锁操作,如果已经创建出来了实例,则无需加锁。所以上面的代码只有在第一个创建实例时才会需要加锁。这样的方法效率已经很好了,只是实现起来比较麻烦,容易出错。
到这里单例模式的懒汉和饿汉模式就都介绍完了,总结一下,懒汉和饿汉分别适用于什么场景。