在GoF著作中对单例模式有这样的描述:“保证一个class只有一个实体,并为它提供一个全局访问点”。看起来很简单的描述实际上设计一个Singleton需要考虑很多因素,具体如下:
1.怎么更好地实行“Singleton的唯一性”
2.Singleton对象的生命周期(管理方案复杂,这篇暂不讨论)
3.multithreading(多线程)问题
静态数据 + 静态函数 != Singleton
乍一看,实现Singleton很简单,如下这种静态成员函数 + 静态成员变量就能实现;但是这种做法有很多缺点:(1)instance_是一个全局变量(它所带来的问题下面会有描述),(2)它的初始化过程很简单没有考虑对象的生命周期,也没有任何清理工作(主要是对象对资源的占用和清理),(3)当然这里还没做到“不允许产生另一个对象”,(4)不适合多线程
file .h
class Singleton
{
public:
static Singleton* Instance()
{
return & instance_;
}
int Dosomething();
private:
static Singleton instance_;
};
file .cpp
Singleton Singleton::instance_;
问题1:为什么全局变量会有问题呢?
***.cpp
#include "Singlenton.h"
Singleton::Instance()->Dosomething();
这里可以看到当***.cpp调用Dosomething()的时候,由于面对不同的编译单元(可以认为是不同的cpp文件)中的动态初始化对象(通过执行期间Singleton的构造函数的调用),c++标准并没有规定他们的初始化顺序,这里你没法确定instance_一定被初始化了,所以Singleton::Instance()的调用返回的可能是一个没有被构造的对象。(这里说一下动态初始化和静态初始化的区别:前者是执行期间初始化,后者是编译期间初始化)
怎么解决这个问题呢,调用者需要用到Singleton::Instance()返回的对象,如果设计成如下这种方式:
Singleton& Singleton::Instance()
{
static Singleton obj;
return obj;
}
函数内的static对象在该函数第一次被执行时被初始化,就可以保证返回的是一个已经被构造的对象了。(这里提一句,static变量如果被一个编译期常量初始化的话,它的值在程序执行之前就已经完成;例如 static int x = 100;函数调用之前这个x的值就已经确定了。当然如果初始值不是编译期,或者是一个拥有构造函数的对象,通常会在第一次执行时被初始化)。
问题2:它没实现唯一性
显然你需要将默认构造函数和copy构造函数以及赋值操作符声明为private,因为你不显示定义这些,编译器会帮你在public段定义;还可以让Instance()返回reference而非指针(防止被调用端delete);不仅仅如此,你还可以将析构函数声明为private,这样防止拥有Singleltn对象指针者意外将其delete。这样的接口看上去如下:
class
{
public:
static Singleton& Instance();
...
private:
Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton();
}
问题3:它的初始化过程很简单没有考虑对象的生命周期,也没有任何清理工作,这个问题很难解决,本次并不讨论
问题4:多线程问题
大家一定想到了时双检锁吧,先看看代码:
file .cpp
Singleton& Singleton::instance()
{
if(!pInstance_)//第一次调用pInstance_也有确定值
{
lock();
if(!pInstance_)
{
pInstance_ = new Singleton;
}
}
return *pInstance_;
}
Singleton Singleton::pInstance_ = nullptr;//静态初始化,
大家一定会觉得这样写肯定能解决多线程问题了吧,其实在RISC(精简指令集)机器的编译器有一个所谓的code arranger,它会重新排列编译器所产生的汇编语言指令,使代码能够最佳运行在处理器上。这样就会导致在lock()之前执行了两次if(!pInstance_),于是双检锁失效。怎么解决呢?至少你需要在pInstance_前添加volatile。