传统写法有什么问题
如果你了解过单例模式,双重检查锁定模式(Double-Checked Locking Pattern,后文简称DCLP)的写法你一定不会陌生,甚至你或许认为它是最正确的代码。
class Singleton {
public:
//获取单例
Singleton* GetInstance() {
//双重检查
if (p_instance==nullptr) {
_mutex.lock();
if (p_instance == nullptr)
p_instance = new Singleton();
_mutex.unlock();
}
return p_instance;
}
private:
//私有构造函数
Singleton() = default;
public:
~Singleton() = default;
private:
//删除构造方法
Singleton& operator=(const Singleton& obj) = delete;
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton&& obj) = delete;
Singleton(const Singleton&& obj) = delete;
private:
static Singleton* p_instance;
mutex _mutex;
};
Singleton* Singleton::p_instance = nullptr;//类外初始化
这几乎是最常见的单例模式,不过上面的代码却有大问题。
问题就出现在这一句:
p_instance = new Singleton();
该问题在Effective C++的作者Scott Meyers关于单例的论文中被深刻讨论,如果你之前对单例模式缺乏了解,这篇论文你一定需要阅读:
C++ and the Perils of Double-Checked Lockinghttps://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
其中提到,要执行该表达式 p_instance = new Singleton(); 需要做三件事:
- 开辟出用于存放单例的内存
- 在开辟的内存上构造Singleton对象
- 使p_instance指针指向开辟的内存
你期望,按照上述步骤去实例化单例,可是编译器没有任何约束去按你期望的顺序执行!
在特殊情况下,编译器有时会调换步骤二和步骤三,就像如下代码:
Singleton* GetInstance() {
//双重检查
if (p_instance==nullptr) {
_mutex.lock();
if (p_instance == nullptr) {
p_instance = static_cast<Singleton*> //步骤三
(operator new(sizeof(Singleton)));//步骤一
new(p_instance) Singleton; //步骤二
}
_mutex.unlock();
}
return p_instance;
}
此时执行顺序变为步骤一,步骤三,步骤二
如果你还没有理解,设想有如下场景:
- 线程A 进入GetInstance,执行完判断,上锁,执行步骤三和步骤一。此时p_instance已经是一个非空的指针,但Singleton单例还未在p_instance所指向的空间上初始化。
- 线程B 进入GetInstance,判断p_instance为非空,随后把p_instance返回。调用处对p_instance解引用,直接G!访问未初始化的内存。
DCLP只会在步骤一,步骤二在步骤三之前执行才能奏效,但他们的执行顺序确是未定义行为!编译器在这一点上不受任何约束,完全可能导致意想不到的问题。
所以用DCLP实现的单例模式,单例对象的初始化顺序不确定。这种情况可能导致在一个线程中访问尚未初始化的单例对象,从而引发错误。 并不是线程安全,因为包含未定义行为。
现代C++实现单例模式
为了规避这个问题,可利用C++11引入 魔法静态变量 特性,主要描述了具有静态存储期或线程存储期的块作用域变量的初始化规则。
§6.7 [stmt.dcl] p4原文: If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
即,如果在变量(具有静态存储期或线程存储期的块作用域的)初始化过程中同时并发地再次进入声明,那么并发执行必须等待初始化过程完成。
利用这项标准,你就可以很爽的写出比DCLP更好的代码!
代码样例:
class Singleton {
public:
//现代写法
Singleton& GetInstance() {
// Let's create a magic static
static Singleton instance;//C++11
return instance;//return refence
}
private:
//私有构造函数
Singleton() = default;
public:
~Singleton() = default;
private:
//删除构造方法
Singleton& operator=(const Singleton& obj) = delete;
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton&& obj) = delete;
Singleton(const Singleton&& obj) = delete;
};
现代写法使用使用局部静态变量来实现单例模式。具体而言,文章提出的Meyers' Singleton就是基于这一思想的解决方案。
如果本文帮助到你,希望能点赞收藏,欢迎留言讨论。