单例模式确保一个类只有一个实例,并提供一个全局访问点。
怎么让某个类只能创建一个实例?
思路:将类的构造函数私有,然后提供一个静态方法访问对象。调用类内成员函数需要对象,但我们又无法创建出对象,所以要将该接口函数声明为静态函数,这样就可以在类外使用类名调用。
class Singleton {
public:
static Singleton* GetInstance() {
if (_uniqueInstance == nullptr) {
_uniqueInstance = new Singleton;
}
return _uniqueInstance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static Singleton* _uniqueInstance;
};
Singleton* Singleton::_uniqueInstance = nullptr;
我们用一个指针保存创建的单例对象,并在第一次调用 GetInstance 时创建对象,以后就直接返回该单例对象。
那么问题来了,如果一个线程判断指针为空,线程创建单例对象。另一个线程在上一个线程创建返回之前,同样进行了判断也得到了指针为空的结果,同样进入创建对象。此时,一个单例对象竟被创建了两次。我们可以采用加锁的方式,让多线程互斥地访问该部分。
class Singleton {
public:
static Singleton* GetInstance() {
_mtx.lock();
if (_uniqueInstance == nullptr) {
_uniqueInstance = new Singleton;
}
_mtx.unlock();
return _uniqueInstance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static Singleton* _uniqueInstance;
static mutex _mtx;
};
Singleton* Singleton::_uniqueInstance = nullptr;
mutex Singleton::_mtx;
此时又有新的麻烦了,明明我们只需要在第一次进入时互斥,后续访问就不再需要了。采用加锁的方式将大大降低程序的运行效率,这在性能要求高的程序中是不可容忍的。
一个比较常见的解决方式是双加锁,首先检查实例是否已经创建了,如果还没创建,才进行互斥控制。这样一来,就只有第一次会进行互斥控制。
static Singleton* GetInstance() {
// 使用 double-check 方式加锁,保证效率和线程安全
if (_uniqueInstance == nullptr) {
_mtx.lock();
if (_uniqueInstance == nullptr) {
_uniqueInstance = new Singleton;
}
_mtx.unlock();
}
return _uniqueInstance;
}
但实际上上面的代码还是有问题的。问题的来源是 CPU 的乱序执行,C++ 里的 new 包含了两个步骤:
所以 ptr = new T
包含了三个步骤:
在这三步中,2 和 3 的顺序是可以交换的。也就是说,有可能:有一个线程分配了内存并将地址赋值给 ptr 了,但还没有初始化该内存。另一线程检测 ptr 不为空,就直接拿去使用了,这时可能引起不可预料的结果。
通常情况下,可以调用 CPU 提供的一条指令来解决该情况,这指令被称为 barrier。一条 barrier 指令会阻止 CPU 将该指令交换到 barrier 之后,也不能将之后的指令交换到 barrier 之前。
#define barrier() __asm__ volaticle("lwsync")
T* ptr = nullptr;
T* GetInstance() {
if (nullptr == ptr) {
lock();
if (nullptr == ptr) {
T* tmp = new T;
barrier();
ptr = tmp;
}
unlock();
}
return ptr;
}
如果程序总是创建并使用单例实例,或者在创建和运行时方面的负担不太繁重,你可以急切创建此单例。
class Singleton {
public:
static Singleton* GetInstance() {
return &_instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static Singleton _instance;
};
Singleton Singleton::_instance;
如果是 Java,上述代码形式没什么问题。JVM 在加载这个类时马上创建此唯一的单例实例。JVM 保证在任何线程访问 uniqueInstance 静态变量之前,一定先创建此实例。
但在 C++ 中还有「不同编译单元内定义的 non-local static 对象」的初始化次序问题。函数内的 static 对象称为 local static 对象,其他 static 对象称为 non-local static 对象。
编译单元(translation unit)是指产出单一目标文件(single object file)的那些源码。基本上它是单一源码文件加上其所含入的头文件。
如果某编译单元内的某个 non-local static 对象的初始化动作使用了另一编译单元内的某个 non-local static 对象,它所用到的这个对象可能尚未被初始化,因为 C++ 对「定义于不同编译单元内的 non-local static 对象」的初始化次序并无明确定义。
幸运的是一个小小的设计便可以解决这个问题。唯一需要做的是:将每个 non-local static 对象搬到自己的专属函数内(该对象在此函数内被声明为 static)。这些函数返回一个 reference 指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。
这个手法的基础在于:C++ 保证,函数呢的 local static 对象会在「该函数被调用期间」「首次遇上该对象的定义式」时被初始化。
class Singleton {
public:
static Singleton& getInstance() {
static Singleton inst;
return inst;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};