说明
首先解释为什么会出现这个问题:在懒汉模式下存在多线程不安全的问题(饿汉模式是线程安全的),为了解决这个问题,首先采用的是利用C++中lock_guard类,这个类实现原理采用RAII,不用手动管理unlock。
class singleton {
private:
singleton() {}
static singleton *p;
static mutex lock_;
public:
static singleton *instance();
};
singleton *singleton::p = nullptr;
singleton* singleton::instance() {
lock_guard guard(lock_);
if (p == nullptr)
p = new singleton();
return p;
}
解决了懒汉模式的这个问题,又出现了性能问题:每次执行都会有加锁释放锁, 而这个步骤只有在第一次new Singleton()才是有必要的.
因此引出DCL
双重检查锁模式
class singleton {
private:
singleton() {}static singleton *p;
static mutex lock_;
public:
singleton *instance();
// 实现一个内嵌垃圾回收类
class CGarbo
{
public:
~CGarbo()
{
if(singleton::p)
delete singleton::p;
}
};
// 2022-4-21
// 所谓的垃圾回收,就是定义一个静态的类,类里面只有一个方法,这个方法检查singleton::p是否是空,不是空,执行delete
// 由于是静态的方法,程序会自动的释放该对象,从而达到自动回收的机制。
static CGarbo Garbo;
// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
};
singleton *singleton::p = nullptr;
singleton::CGarbo Garbo;
singleton* singleton::instance() {
if (p == nullptr) {
lock_guard guard(lock_);
if (p == nullptr)
p = new singleton();
}
return p;
}
注意我们的标题是单例模式双重锁漏洞。
DCLP的关键在于,大多数对instance的调用会看到p是非空的,因此甚至不用尝试去初始化它。因此,DCLP在尝试获取锁之前检查p是否为空。只有当检查成功(也就是p还没有被初始化)时才会去获得锁,然后再次检查p是否仍然为空(因此命名为双重检查锁)。第二次检查是必要,因为就像我们刚刚看到的,很有可能另一个线程偶然在第一次检查之后,获得锁成功之前初始化p。
看起来上述代码非常美好,可是过了相当一段时间后,才发现这个漏洞,原因是:内存读写的乱序执行(编译器问题)。
再次考虑初始化p的那一行:
p = new singleton;
这条语句会导致三个事情的发生:
分配能够存储singleton对象的内存;
在被分配的内存中构造一个singleton对象;
让p指向这块被分配的内存。
可能会认为这三个步骤是按顺序执行的,但实际上只能确定步骤1是最先执行的,步骤2,3却不一定。问题就出现在这。
线程A调用instance,执行第一次p的测试,获得锁,按照1,3,执行,然后被挂起。此时p是非空的,但是p指向的内存中还没有Singleton对象被构造。
线程B调用instance,判定p非空, 将其返回给instance的调用者。调用者对指针解引用以获得singleton,噢,一个还没有被构造出的对象。bug就出现了。
DCLP能够良好的工作仅当步骤一和二在步骤三之前被执行,但是并没有方法在C或C++中表达这种限制。这就像是插在DCLP心脏上的一把匕首:我们需要在相对指令顺序上定义限制,但是我们的语言没有给出表达这种限制的方法。
第一种解决方法:memory barrier指令 第一种实现:
【C++单例模式双重锁漏洞(内存读写的乱序执行(编译器问题))】基于operator new+placement new,遵循1,2,3执行顺序依次编写代码。
// method 1 operator new + placement new
singleton *instance() {
if (p == nullptr) {
lock_guard guard(lock_);
if (p == nullptr) {
singleton *tmp = static_cast(operator new(sizeof(singleton)));
new(tmp)singleton();
p = tmp;
}
}
return p;
}
第二种实现:
基于直接嵌入ASM汇编指令mfence,uninx的barrier宏也是通过该指令实现的。
#define barrier() __asm__ volatile ("lwsync")
singleton *singleton::instance() {
if (p == nullptr) {
lock_guard guard(lock_);
barrier();
if (p == nullptr) {
p = new singleton();
}
}
return p;
}
推荐阅读
- 机试编程题|HJ33 整数与IP地址间的转换
- 算法|2020 必学的10大顶级 Python 开源库
- 蓝桥杯|2022年蓝桥杯省赛真题解析(C++B组)
- c++|2022十三届蓝桥杯体验
- 蓝桥杯|浅谈2022第十三届蓝桥杯c/c++b组
- 令人快乐的刷题小妙招|【2022第十三届蓝桥杯】c/c++ 大学c组 解题报告
- YOLO|Ubuntu18.04配置darknet环境实现YOLOv4目标检测(五)——darknet YOLOv4和YOLOv4-tiny模型转ONNX转TensorRT部署
- c++|C++中关于进制输出的总结
- C++避坑指南|vector.earse()避坑指南(引发了异常: 读取访问权限冲突。_Mycont 是 nullptr。)