单例模式只涉及到一个单一的类,该类让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。
借用https://refactoringguru.cn/design-patterns/singleton中的图就是:
单例模式包含如下角色:
- 单例类:创建并维持一个唯一实例的类;
- 访问类:使用单例类。
单例模式优点:
- 保证一个类只有一个实例,这对于线程池和连接池等池化对象很有意义。
- 仅需要在首次请求单例对象时对其进行初始化。
单例模式缺点:
- 单例模式同时解决了保证一个类只有一个实例和为该实例提供一个全局访问节点两个问题, 所以违反了单一职责原则。
- 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
单例设计模式分为两种:
- 饿汉式:类加载时就会创建该单实例对象。
- 懒汉式:单实例对象不会在类加载时被创建,而是在首次使用该对象时创建。
一、饿汉式
饿汉模式的优点是线程安全,但它的一个很明显缺点是单例创建后不一定会立即被使用,会造成一定的内存浪费。
除此以外,饿汉模式还有一个潜在的问题,那就是如果程序中有多个单例类,它们会面临静态初始化顺序问题,即全局变量会在 main 函数之前初始化完成,但是多个全局变量之间并没有确定的初始化顺序。这在面对嵌套单例时更为明显,外层单例类很有可能先于内层单例类构造,我们在编写代码时也不能假设某个全局变量在另一个全局变量之前初始化完成。
#include <iostream>
/* 通过静态变量实现的单例类 */
class Cat {
private:
/* 让构造函数私有以避免类被实例化 */
Cat() = default;
/* 类对应的唯一的对象 */
static Cat *m_instance;
public:
/* 实例对象的唯一访问方式 */
static Cat *getInstance() {
return m_instance;
}
};
/* 静态成员变量需要类内定义类外初始化 */
Cat *Cat::m_instance = new Cat;
int main() {
Cat *dragonLi = Cat::getInstance();
Cat *ragdoll = Cat::getInstance();
std::cout << dragonLi << std::endl;
std::cout << ragdoll << std::endl;
delete dragonLi;
return 0;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
0x6000007dc040
0x6000007dc040
atreus@MacBook-Pro %
二、懒汉式
1.第一种实现方式(Gamma Singleton)
#include <iostream>
using namespace std;
/* 通过静态变量实现的单例类 */
class Cat {
private:
static Cat *instance; // 单例类对应的唯一的对象
/* 让构造函数私有以避免类被实例化 */
Cat() {
cout << "Cat()" << endl;
}
public:
/* 实例对象的唯一访问方式 */
static Cat *getInstance() {
// 多线程场景下此处需要加锁
instance = (instance == nullptr) ? new Cat : instance;
return instance;
}
~Cat() {
cout << "~Cat()" << endl;
}
};
/* 静态成员变量需要类内定义类外初始化 */
Cat *Cat::instance = nullptr;
int main() {
Cat *dragonLi = Cat::getInstance();
Cat *ragdoll = Cat::getInstance();
std::cout << dragonLi << std::endl;
std::cout << ragdoll << std::endl;
delete dragonLi;
// delete ragdoll; // 由于两个猫咪实际上是一个单例 所以会发生内存的重复释放
return 0;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
Cat()
0x600001044040
0x600001044040
~Cat()
atreus@MacBook-Pro %
这种实现方法主要存在两个问题:
- 线程不安全。需要在实例化单例前进行加锁,不然很有可能在前一个线程判空后新建单例前后一个线程完成判空,导致实例化多个单例。
- 内存安全也存在一定风险。一方面是可能忘记 delete,内存安全完全托管给了 OS;另一方面在一些特殊的情况下也有可能出现上述的重复释放问题。
对于内存安全的问题,一种解决方案是将单例模式与 RAII 机制结合使用,通过增加一个 RAII 类来保证内存安全;另一种更简洁的解决方法则是基于静态局部变量来实现单例类(Meyers Singleton)。
/* 以 RAII 方式来控制单例 */
class CatStore {
public:
Cat *m_cat;
explicit CatStore(Cat *cat) {
m_cat = cat;
}
~CatStore() {
delete m_cat;
}
Cat *getCat() const {
return m_cat;
}
};
int main() {
CatStore catStore(Cat::getInstance());
Cat *dragonLi = catStore.getCat();
Cat *ragdoll = catStore.getCat();
std::cout << dragonLi << std::endl;
std::cout << ragdoll << std::endl;
return 0;
}
2.第二种实现方式(Meyers Singleton 基于静态局部变量)
#include <iostream>
using namespace std;
/* 通过静态变量实现的单例类 */
class Cat {
private:
/* 让构造函数私有以避免类被实例化 */
Cat() {
cout << "Cat()" << endl;
}
~Cat() {
cout << "~Cat()" << endl;
}
public:
/* 实例对象的唯一访问方式 */
static Cat *getInstance() {
static Cat instance; // 静态成员函数里的静态局部变量等效于类的静态成员变量
return &instance;
}
};
int main() {
{
Cat *dragonLi = Cat::getInstance();
std::cout << dragonLi << std::endl;
}
Cat *ragdoll = Cat::getInstance();
std::cout << ragdoll << std::endl;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
Cat()
0x100e10000
0x100e10000
~Cat()
atreus@MacBook-Pro %
首先,对于静态局部变量来说,static 关键字并没有改变它的局部作用域,当定义它的函数或者语句块结束时(如上图中的大括号结束),作用域也随之结束。
但是当静态局部变量离开作用域后,它并没有销毁,仍然驻留在内存当中,只是暂时无法被访问,只要我们再次调用 getInstance() 方法,就能重新得到这个静态局部变量。
当程序结束时,该静态局部变量的内存会被自动释放,单例类也会被自动析构,因此内存安全得以保证,上面主函数的执行结果也印证了这一点。
其次,对于线程安全的问题,GCC 等编译器已经支持了静态变量构造和析构函数的多线程安全。以构造函数为例,对于局部静态变量,多线程调用时,首先构造静态变量的线程先加锁,其他线程等锁,因此线程安全也得以保证。
实际上,静态局部变量的多线程安全是与编译选项 -fno-threadsafe-statics
直接挂钩的,而此选项在不同编译器中都默认打开。