单例模式
1. 线程不安全下的单例模式
懒汉模式
所谓懒汉模式,就是在第一次使用的时候再去实例化。
class singleton {
private:
singleton() {}
static singleton* p;
public:
static singleton* getInstance();
};
singleton* singleton::p = nullptr;
singleton* singleton::getInstance() {
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* getInstance();
};
singleton* singleton::p = new singleton();
singleton* singleton::getInstance() {
return p;
}
-
当然这个是线程安全的,对于我们通常阐述的线程不安全,为懒汉模式,下面会阐述懒汉模式的线程安全代码优化。
-
饿汉模式虽好,但其存在隐藏的问题,在于非静态对象(函数外的static对象)在不同编译单元中的初始化顺序是未定义的。如果在初始化完成之前调用
getInstance()
方法会返回一个未定义的实例。
2. 懒汉模式下的线程加锁-双加锁模式
class singleton {
private:
singleton() {}
static singleton *p; // 单例对象
static mutex mtx; // 互斥量
public:
static singleton* instance();
void doSomething(){
cout << "do Something\n";
}
class GC // 实现一个内嵌垃圾回收类
{
public:
~GC()
{
if(p){
delete p;
cout << "delete p\n";
}
}
};
static GC gc; // 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
};
//在类外初始化静态对象
singleton* singleton::p = nullptr;
mutex singleton::mtx;
singleton::GC singleton::gc; // 一定要初始化,不然程序结束时不会析构gc
singleton* singleton::instance() {
if (p == nullptr) {
lock_guard<mutex> guard(mtx);
if (p == nullptr)
p = new singleton();
}
return p;
}
为什么要加两次锁?
-
如果是下面这种形式,只检测一次,想象一下,在每次调用获取实例的方法时,都需要加锁,这将严重影响程序性能。双层检测可以有效避免这种情况,仅在第一次创建单例的时候加锁,其他时候都不再符合NULL == p的情况,直接返回已创建好的实例。
single* single::getInstance(){ pthread_mutex_lock(&lock); if (NULL == p){ // 第二次检查 p = new single; } pthread_mutex_unlock(&lock); return p; }
双重锁一定就安全么?
3. Effective C++ 中的写法-局部静态变量
class single{
private:
single(){}
~single(){}
public:
static single* getinstance();
};
single* single::getinstance(){
static single obj;
return &obj;
}
- 单线程下,正确。
- C++11及以后的版本(如C++14)的多线程下,正确。
- C++11之前的多线程下,不一定正确。
为什么在c++ 11 之前线程不安全?
原因在于在C++11之前的标准中并没有规定local static变量的内存模型。于是乎它就是不是线程安全的了。但是在C++11却是线程安全的,这是因为新的C++标准规定了当一个线程正在初始化一个变量的时候,其他线程必须得等到该初始化完成以后才能访问它。