浅谈C++单例模式

一 简介
单例模式,指的是某一个类,只允许实例出一个对象存在。而实现单例模式有懒汉式和饿汉式。饿汉式指的是在创建类时就初始化好对象,,而懒汉式指的是在需要使用到对象实例时,才进行初始化对象。

二 实现方式
1.最基本的实现方式
#include
#include

class SingLeton
{
private:
SingLeton() {};
static SingLeton *p;

public:
static SingLeton *initstance();
};

SingLeton* SingLeton::p = nullptr;

SingLeton* SingLeton::initstance() {
if (p == nullptr) {
p = new SingLeton();
}
return p;
}

这个代码只适合在单线程下,当多线程时,是不安全的。如果两个线程同时首次调用instance方法且同时检测到p是nullptr,则两个线程会同时构造一个实例给p,这将违反了单例的准则。

2.给线程加锁,当线程A在执行p = new Singleton()的时候,线程B如果调用了instance(),一定会被阻塞在加锁处,等待线程A执行结束后释放这个锁。从而是线程安全的。

3.懒汉与饿汉
单例分为两种实现方法:

懒汉
第一次用到类实例的时候才会去实例化,上述就是懒汉实现。
饿汉
单例类定义的时候就进行了实例化。
这里也给出饿汉的实现:

class singleton {
private:
singleton() {}
static singleton *p;
public:
static singleton *instance();
};

singleton singleton::p = new singleton();
singleton
singleton::instance() {
return p;
}
这个线程是安全的

4.双重检查锁模式
什么是双重锁(DCLP):假设你有一个类,它实现了著名的单例模式,现在你想让它变得线程安全。显然的一个方法就是通过增加一个锁来保证互斥共享。这样的话,如果有两个线程同时调用了Singleton::initance,将只有其中之一会创建这个单例。这是完全合法的方法,但是一旦单例被创建,实际上就不再需要锁了。锁不一定慢,但是在高并发的条件下,不具有很好的伸缩性。双重检查锁定模式是由此得名:在单例指针p为NULL的时候,我们仅仅使用了一个锁,这个锁使偶然访问到该单例的第一组线程继续下去。而在锁的内部,p被再次检查,这样就只有第一个线程可以创建这个单例了。

多线程加锁的情况是不管任何情况都会去加锁,然后释放锁,而对于读操作是不存在线程安全的,故只需要在第一次实例创建的时候加锁,以后不需要。

singleton* singleton::instance() {
if(p == nullptr) { // 第一次检查
Lock lock;
if(p == nullptr){ // 第二次检查
p = new singleton;
}
}
return p;
}
基于上述,我们可以写出双重检查锁+自动回收

class singleton {
private:
singleton() {}

static singleton *p;
static mutex lock_;

public:
singleton *instance();

// 实现一个内嵌垃圾回收类
class CGarbo
{
public:
    ~CGarbo()
    {
        if(singleton::p)
            delete singleton::p;
    }
};
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;
}

看起来上述代码非常美好,可是过了相当一段时间后,才发现这个漏洞,原因是:内存读写的乱序执行(编译器问题)。

再次考虑初始化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心脏上的一把匕首:我们需要在相对指令顺序上定义限制,但是我们的语言没有给出表达这种限制的方法。

5.memory barrier指令
DCLP问题在C++11中,这个问题得到了解决。

因为新的C++11规定了新的内存模型,保证了执行上述3个步骤的时候不会发生线程切换,相当这个初始化过程是“原子性”的的操作,DCL又可以正确使用了,不过在C++11下却有更简洁的多线程singleton写法了

C++11之前解决方法是barrier指令。要使其正确执行的话,就得在步骤2、3直接加上一道memory barrier。强迫CPU执行的时候按照1、2、3的步骤来运行。

第一种实现:

基于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<singleton *>(operator new(sizeof(singleton)));
new(tmp)singleton();
p = tmp;
}
}
return p;
}

参考:
https://www.jianshu.com/p/05c3ff714e70
github上C++那些事

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值