C++ 单例模式
众所周知的单例
class singleton {
private:
singleton() {}
static singleton *p;
public:
static singleton *instance();
};
singleton *singleton::p = nullptr;
singleton* singleton::instance() {
if (p == nullptr)
p = new singleton();
return p;
}
这是一个非常简单的实现,将构造函数声明为private或protect防止被外部函数实例化,内部有一个静态的类指针保存唯一的实例,实例的实现由一个public方法来实现,该方法返回该类的唯一实例。
当然这个代码只适合在单线程下,当多线程时,是不安全的。考虑两个线程同时首次调用instance方法且同时检测到p是nullptr,则两个线程会同时构造一个实例给p,这将违反了单例的准则。
懒汉和饿汉
单例分为两种实现方法:
- 懒汉
- 第一次用到类实例的时候才会去实例化,上述就是懒汉实现。
- 饿汉
- 单例类定义的时候就进行了实例化。
这里也给出饿汉的实现:
class singleton {
private:
singleton() {}
static singleton *p;
public:
static singleton *instance();
};
singleton *singleton::p = new singleton();
singleton* singleton::instance() {
return p;
}
当然这个是线程安全的,对于我们通常阐述的线程不安全,为懒汉模式,下面会阐述懒汉模式的线程安全代码优化。
加锁确保线程安全
#include <mutex>
using namespace std;
class singleton {
private:
singleton() {}
static singleton *p;
static mutex *lock_;
public:
static singleton *instance();
};
mutex *singleton::lock_ = new mutex();
singleton *singleton::p = nullptr;
singleton* singleton::instance() {
lock_guard<mutex> guard(*lock_);
if (p == nullptr)
p = new singleton();
return p;
}
在C++中加锁有个类不用手动管理unlock,那就是lock_guard,这里采用其进行加锁。
这种写法不会出现上面两个线程都执行到p==nullptr里面的情况,当线程A在执行new singleton()的时候,线程B如果调用了instance(),一定会被阻塞在加锁处,等待线程A执行结束后释放这个锁。从而是线程安全的。
但是这种写法性能非常低下,因为每次调用instance()都会加锁释放锁,而这个步骤只有当在第一次new singleton()才是有必要的,只要p被创建出来了,不管多少线程同时访问,if (p == nullptr)进行判断都是足够的(只是读操作,不需要加锁),没有线程安全问题,加了锁之后反而存在性能问题。
双重检查锁模式
上面写法是不管任何情况都会去加锁,然后释放锁,而对于读操作是不存在线程安全问题的,故只需要在第一次实例创建的时候加锁,以后不需要。
#include <mutex>
using namespace std;
class singleton {
private:
singleton() {}
static singleton *p;
static mutex *lock_;
public:
static singleton *instance();
};
mutex *singleton::lock_ = new mutex();
singleton *singleton::p = nullptr;
singleton* singleton::instance() {
if(p == nullptr) {
lock_guard<mutex> guard(*lock_);
if(p == nullptr){
p = new singleton;
}
}
return p;
}
看起来上述代码非常美好,可是过了相当一段时间后,才发现这个漏洞。考虑以下语句
p = new singleton;
这条语句会导致三个事情的发生:
- 分配能够存储
singleton对象的内存; - 在被分配的内存中构造一个
singleton对象; - 让
p指向这块被分配的内存。
你可能会认为这三个步骤是按顺序执行的,但实际上只能确定步骤1是最先执行的,步骤2,3却不一定。问题就出现在这。在汇编级别层面分析以下过程:
- 线程A调用instance,执行第一次p的测试,获得锁,按照1,3,执行,然后被挂起。此时p是非空的,但是p指向的内存中还没有singleton对象被构造。
- 线程B调用instance,判定p非空, 将其返回给instance的调用者。调用者对指针解引用以获得singleton,噢,一个还没有被构造出的对象。bug就出现了。