定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式的最终目的是为了要实现对唯一实例 的受控访问。另外注意,由于子类对象中会包含有基类的子对象,因此使用单例模式的类不能够作为基类去派生其他的类。
单例模式结构图:
image.png
根据对对象进行初始化时机的不同,单例模式可以分为懒汉式和饿汉式
懒汉式
懒汉式单例模式是指系统运行中,实例并不存在,只有当需要使用该实例时,才会去创建并使用实例。(这种方式要考虑线程安全)
C++ 11 以前的标准
双检查锁实现懒汉式单例模式(Double-checking Lock)
class Singleton{
public:
static Singleton* getInstance(){
// 先检查对象是否存在
if(m_instance==nullptr){
Lock lock; //伪代码
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}
private:
Singleton(); //私有构造函数,不允许使用者自己生成对象
Singleton(const Singleton& other);
static Singleton* m_instance; //静态成员变量
};
在相当长的一段时间,迷惑了很多人,在2000
年的时候才被人发现漏洞,而且在每种语言上都发现了。原因是内存读写的乱序执行(编译器的问题)。
分析:m_instance = new Singleton()
这句话可以分成三个步骤来执行:
-
分配了一个
Singleton
类型对象所需要的内存。 -
在分配的内存处构造
Singleton
类型的对象。 -
把分配的内存的地址赋给指针
m_instance
。
可能会认为这三个步骤是按顺序执行的,但实际上只能确定步骤1
是最先执行的,步骤2
,3
却不一定。问题就出现在这。假如某个线程A在调用执行m_instance = new Singleton()
的时候是按照1,3,2
的顺序的,那么刚刚执行完步骤3
给Singleton
类型分配了内存(此时m_instance
就不是nullptr
了)就切换到了线程B
,由于m_instance
已经不是nullptr
了,所以线程B
会直接执行return m_instance
得到一个对象,而这个对象并没有真正的被构造!!严重bug就这么发生了。
C++ 11中的版本
C++11当中明确保证了 static local 对象是线程安全的,同时由于 static local object 是存放于静态区,因此不会出现内存泄漏的问题
public:
static Singleton& GetInstance(){
static Singleton s;
return s;
}
private:
Singleton(){}
~Singleton(){}
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
};
饿汉式
饿汉式是指系统一运行,就初始化创建实例,当需要时,直接调用即可。(本身就线程安全,没有多线程的问题)
class Singleton
{
private:
static Singleton instance;
Singleton();
~Singleton();
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
return instance;
}
}
// initialize defaultly
Singleton Singleton::instance;
在饿汉式单例模式中,instance 是全局静态对象,因此初始化的过程将会在 main 之前的单线程启动阶段,因此不存在线程安全问题。
补充内容:static 对象的初始化
全局 static 对象(函数外)
C++规定,non-local static 对象的初始化发生在main函数执行之前,也即main函数之前的单线程启动阶段,所以不存在线程安全问题。但C++没有规定多个non-local static 对象的初始化顺序,尤其是来自多个编译单元的non-local static对象,他们的初始化顺序是未定义的
局部 static 对象(函数内)
对于local static 对象,其初始化发生在控制流第一次执行到该对象的初始化语句时。多个线程的控制流可能同时到达其初始化语句。
在C++11之前,在多线程环境下local static对象的初始化并不是线程安全的。具体表现就是:如果一个线程正在执行local static对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该local static对象的构造函数中。这会造成这个local static对象的重复构造,进而产生内存泄露问题。所以,local static对象在多线程环境下的重复构造问题是需要解决的。
而C++11则在语言规范中解决了这个问题。C++11规定,在一个线程开始local static 对象的初始化后到完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待,直到该local static 对象初始化完成。
网友评论