单例模式
⼀个类仅有⼀个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
那么,我们必须保证:该类不能被复制;也不能被公开的创造。
对于 C++ 来说,它的构造函数,拷贝构造函数和赋值函数都不能被公开调用。
单例模式又分为 懒汉模式 和 饿汉模式 ,它们之间各有好处:
-
懒汉模式的实例在第一次被引用时才进行初始化,支持延迟加载,资源利用效率更高;但是当资源访问频繁时,资源同步问题(加锁、解锁)会限制并发性能,也就是不支持高并发。
-
饿汉模式提前初始化实例,启动时间较长;但是可以避免资源同步(加锁、解锁)带来的性能消耗,后续的响应时间更好。
饿汉模式
在加载类时,对象实例就被创建并初始化,在程序结束时自动销毁,因此,它是线程安全的。
cpp
//.h文件
class Singleton {
public:
static Singleton* GetInstance(){
return _Instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator = (const Singleton&) = delete;
private:
static Singleton* _Instance;
};
// .CPP文件
Singleton* Singleton::_Instance = nullptr; // 类外初始化,必须写
懒汉模式
在 C++11 标准中,静态局部变量 的初始化是线程安全的,因此,可以用以下方式来编写"懒汉"模式:
cpp
class Singleton {
public:
static Singleton& GetInstance() {
static Singleton instance;
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator = (const Singleton&) = delete;
};
更加传统的写法如下,使用 双重检查锁定 来确保线程安全,通过静态成员函数 Destructor
来解决内存泄漏:
cpp
//代码实例(线程安全)
emplate<typename T>
class Singleton {
public:
static T* getInstance() {
if (_instance == nullptr) {
lock_guard<mutex> lock(_mutex);
if (_instance == nullptr) { // 双重检查锁定, 确保线程安全
_instance = new T();
atexit(Destructor); // 在程序退出的时候释放资源
}
}
return _instance;
}
private:
// 防止外界构造/拷贝/删除对象
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator = (const Singleton&) = delete;
static void Destructor() {
if (_instance != nullptr) {
delete _instance;
_instance = nullptr;
}
}
// 静态指针
static T* _instance;
// 互斥锁
static mutex _mutex;
};
// 初始化静态指针
template<typename T>
T* Singleton<T>::_instance = nullptr;
// 初始化互斥锁
template<typename T>
mutex Singleton<T>::_mutex;
但是,这种写法可能出现以下问题:
由于 new 操作分为三步:分配内存、返回指针、调用构造函数;如果一个线程在分配内存后返回了指针,但还没有构造对象,另一个线程就尝试访问该对象的情况,就可能会出现未定义行为或错误。
因此,还是推荐使用"静态局部变量"的写法。
单例的应用
需要强调的是,单例只是一种组织全局变量和静态函数的方式 。
当我们想要拥有应用于某种全局数据集的功能,并且我们想要重复使用时,单例是非常有用的。比如以下场景:
-
配置管理;
-
日志记录;
-
消息队列;
-
线程池、连接池、内存池、对象池。
但是,单例模式是存在弊端的:
- 单例模式会隐藏类之间的依赖关系。
由于单例类不需要显示地创建,也不需要依赖参数传递,在函数中直接调用就好,所以在阅读代码时,需要仔细阅读才能清楚哪些类依赖了单例类。
- 单例模式的拓展性较差。
单例类只能创建一个实例,如果哪天需要在代码中创建多个实例,则需要对代码进行较大的改动。
以数据库连接池为例,假设一开始我们将其设计为一个单例类,而后我们发现,有些 SQL 语句的执行效率低下,长时间占用连接资源,因此我们希望再创建一个连接池实例,让它专门处理运行速度较慢的 SQL 语句,而此时,单例模式就对代码的拓展性产生了影响。