单例模式
(Singleton Pattern)是一种创建型设计模式,限制一个类只能有一个实例化对象,并提供一个全局访问方式。
- 优点
不适用于频繁变化的对象、扩展困难、可能违背单一职责原则等
。
- 使用场景
- 游戏引擎核心管理器: 确保游戏引擎的核心功能只有一个实例,提供全局访问点。
- 资源管理器: 管理游戏中的资源,如纹理、声音等,确保资源的唯一性和高效访问。
- 全局配置管理:系统中的配置信息通常需要被多个组件共享,使用单例模式可以确保所有的组件访问的是同一个配置对象。
- 线程池管理:线程池是执行后台任务的理想选择,单例模式可以确保整个应用中只存在一个线程池实例,避免资源浪费。
- 数据库连接池:数据库连接是一种宝贵的资源,使用单例模式可以有效地管理这些连接,确保不会创建过多的连接实例。
- 日志记录器:日志记录器通常需要在整个应用中共享,以便于跟踪和记录日志信息,单例模式可以确保所有日志调用都指向同一个日志记录器实例。
- Web应用的配置对象:Web应用中的配置对象,如Spring的ApplicationContext,通常使用单例模式来确保整个应用共享同一个配置上下文。
- 操作系统的特定服务:例如,Windows的Task Manager(任务管理器)和Recycle Bin
单例模式的实现
- 懒汉模式
- 饿汉模式
饿汉式单例模式指的是在类加载时就创建唯一实例。这种实现方式能保证线程安全,因为类加载时的操作是线程安全的。但是,由于实例在类加载时就创建,无论是否需要使用都会占用资源,可能导致资源浪费。
个人习惯是推荐用饿汉模式,整体简单,代码好理解. 已经用单例了还考虑资源占用问题, 对于服务端开发而言没意义.因为这点资源开服期间能加载是比较好的,避免中途加载耗费大量cpu.
单例模式代码实现细节
- 互斥锁
懒汉式单例模式即在第一次获取单例时才创建单例
#include <iostream>
using namespace std;
class Singleton{
private:
// 构造函数要设置为私有的,防止外部直接调用创建实例
Singleton() {}
// 删除拷贝构造函数,防止拷贝实例
Singleton(const Singleton&) = delete;
// 删除赋值操作符,防止复制实例
Singleton& operator =(const Singleton&) = delete;
// 静态成员变量,保存唯一实例
static Singleton *m_instance;
static mutex m_mutex;
public:
static Singleton *getInstance() {
// 为了线程安全加互斥锁,但是每次访问都得加锁,开销大
lock_guard<mutex> lock(m_mutex); // 加锁
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
};
Singleton* Singleton::m_instance = nullptr;
mutex Singleton::m_mutex;
为了线程安全加互斥锁,导致是每次访问都得加锁开销大,考虑采用双重检查锁定
- 双重检查锁定
static Singleton *getInstance() {
if (m_instance == nullptr) { // 第一次检查,如果实例已经被创建,则不用加锁
lock_guard<mutex> lock(m_mutex);
if (m_instance == nullptr) { // 第二次检查
m_instance = new Singleton();
}
}
return m_instance;
}
这行代码在实际操作中分为三步:
- ① 分配内存
- ② 调用构造函数初始化对象
- ③ 将内存地址赋值给m_instance
指令重排就可能会导致步骤③提前到步骤②之前进行,那么当运行完步骤③之后,m_instance就已经是非空了,但此时步骤②还没执行,如果此时有另外一个线程来获取实例,就会获取到一个未完全初始化的实例,从而导致程序行为异常,道阻且艰,让我继续优化
- 内存屏障
#include <iostream>
#include <mutex>
#include <atomic>
using namespace std;
class Singleton{
private:
// 构造函数要设置为私有的,防止外部直接调用创建实例
Singleton() {}
// 删除拷贝构造函数,防止拷贝实例
Singleton(const Singleton&) = delete;
// 删除赋值操作符,防止复制实例
Singleton& operator =(const Singleton&) = delete;
// 静态成员变量,保存唯一实例
static atomic<Singleton *> m_instance; // 声明为原子类型
static mutex m_mutex;
public:
static Singleton *GetInstance() {
Singleton *tmp = m_instance.load(memory_order_acquire);
if (tmp == nullptr) {
lock_guard<mutex> lock(m_mutex);
tmp = m_instance.load(memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton();
m_instance.store(tmp, memory_order_release);
}
}
return m_instance;
}
};
Singleton* Singleton::m_instance = nullptr;
mutex Singleton::m_mutex;
首先,将m_instance声明为
atomic原子变量,确保对它的读写操作都是原子操作。
在第一次检查时使用
memory_order_acquire,保证在当前线程读取某个原子变量之前,所有其他线程对该变量的写操作都已经完成,且对其他线程可见,意思就是说:所获取到的值一定是写入之后的结构,保证了读写顺序。
赋值的时候使用
memory_order_release,确保之前的写操作不会被重排到当前操作之后,意思就是说,在tmp赋值给m_instance之前,tmp一定是完成了构造函数初始化的,这样就确保m_instance中一定是一个已完成构造的完整实例。
(C++11之前的也有对应的内存屏障函数可用,也可解决该问题)
看到这里麻了没?以为简简单单的单例模式,竟然有这么多坑,竟然需要这么长的一段代码,别急,图灵大佬们早就替我们想到了更好的解决方式,接着往下看。
- 使用局部静态变量
在C++11及其以上版本,支持使用局部静态变量的线程安全初始化,可以利用这一特性实现线程安全的单例模式。
#include <iostream>
using namespace std;
class Singleton{
private:
// 构造函数要设置为私有的,防止外部直接调用创建实例
Singleton() {}
// 删除拷贝构造函数,防止拷贝实例
Singleton(const Singleton&) = delete;
// 删除赋值操作符,防止复制实例
Singleton& operator =(const Singleton&) = delete;
public:
static Singleton &getInstance() {
static Singleton m_instance; // 局部静态变量,线程安全
return m_instance;
}
};
那有人要问了,为啥这个局部静态变量就一定是线程安全的?
其实是C++11标准中明确规定了局部静态变量的初始化必须保证线程安全,编译器会在底层实现中自动加入现成同步机制,确保多个线程同时调用时,局部静态变量只会被初始化一次,原理和我们上面几种方法的代码差不多,只是不由我们自己实现了而已,该有的性能损耗还是有的。
- call_once函数模板
C++11中还提供了一个std::call_once的函数模板,可以确保某个函数在多个线程中只被调用一次,原型如下:
template<typename _Callable, typename... _Args>
void call_once(std::once_flag& flag, _Callable&& func, _Args&&...args);
我们使用call_once对我们的代码再次进行改动:
#include <iostream>
#include <mutex>
using namespace std;
class Singleton {
private:
// 构造函数要设置为私有的,防止外部直接调用创建实例
Singleton() {}
// 删除拷贝构造函数,防止拷贝实例
Singleton(const Singleton&) = delete;
// 删除赋值操作符,防止复制实例
Singleton& operator =(const Singleton&) = delete;
static Singleton *m_instance;
static once_flag m_flag;
void createInstance() {
m_instance = new Singleton();
}
public:
static Singleton *getInstance() {
call_once(m_flag, createInstance); // 确保只调用一次
return m_instance;
}
};
Singleton *Singleton::m_instance = nullptr;
once_flag Singleton::m_flag;
参考:
[1]
https://zhuanlan.zhihu.com/p/22489545167 这篇对单例模式讲述正确性较高
[2]
https://zhuanlan.zhihu.com/p/696460150 讲述单例模式的应用场景