10.单例模式 (Singleton Pattern)
单例模式的定义
单例模式(Singleton Pattern) 是一种创建型设计模式,确保一个类在整个程序生命周期中只能有一个实例,并提供一个全局访问点。
特点:
- 唯一性:保证系统中某个类只能有一个实例。
- 全局访问点:提供全局的静态方法来访问这个唯一实例。
- 懒加载(Lazy Initialization):实例只会在第一次访问时创建,节省资源。
适用场景
单例模式常用于以下场景:
- 管理共享资源(如数据库连接池、线程池、日志管理器)。
- 全局状态管理(如游戏引擎中的配置管理器)。
- 设备驱动(如打印机管理类,确保同时只有一个任务访问)。
- 缓存(如 DNS 解析缓存)。
- 配置管理(如应用程序的配置文件管理)。
在软件系统中,经常有这样一些特殊类,必须保证它们在系统中只存在一个实例,才能确保他们的逻辑正确性,以及良好的效率。
如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?
class Singleton{
private:
Singleton();
Singleton(const Singleton& other);
public:
static Singleton* getInstance();
static Singleton* m_instance;
};
Singleton* Singleton::m_instance=nullptr;
//线程非安全版本
Singleton* Singleton::getInstance() {
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
上述这种实现在多线程的情况下并不安全。解决这种情况可以加锁,但是锁的代价太大,因为每次访问都会被锁。
//线程安全版本,但锁的代价过高
Singleton* Singleton::getInstance() {
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
加双检查锁(锁前锁后双检查)就可以避免上述这个问题:
//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {
if(m_instance==nullptr){
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}
上述这种实现仍然会存在一些问题。线程是在指令层次抢时间片,也就是 m_instance = new Singleton(); 这一行代码可能会先分配内存,然后把内存地址给 m_instance,然后再执行构造器。这样的话就会存在问题,当线程1还没有执行构造函数,但是已经有了地址 m_instance 之后,另外一个线程进来发现 m_instance 不是nullptr就直接返回了这个地址。但是这个地址是不能用的,因为还没有调用构造器。
经典的 Singleton 实现(C++11 之前)
在 C++11 之前,实现单例模式时,需要考虑线程安全和双重检查锁定(DCLP, Double-Checked Locking Pattern):
class Singleton {
private:
static Singleton* instance; // 静态指针,指向唯一实例
static std::mutex m_mutex; // 互斥锁,确保线程安全
Singleton() {} // 构造函数私有,防止外部创建对象
~Singleton() {}
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* getInstance() {
if (instance == nullptr) { // 第一次检查(避免不必要加锁)
std::lock_guard<std::mutex> lock(m_mutex);
if (instance == nullptr) { // 第二次检查,确保不会创建多个实例
instance = new Singleton();
}
}
return instance;
}
};
// 初始化静态成员
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::m_mutex;
缺点:
- 需要手动管理 instance 的内存释放,可能导致内存泄漏。
- 在多线程环境下,需要加锁,可能会影响性能。
C++11 之后的跨平台实现
C++11 提供了 std::atomic 和 std::mutex,使得单例模式可以更加高效和安全。
#include <atomic>
#include <mutex>
class Singleton {
private:
static std::atomic<Singleton*> m_instance; // 使用 atomic 保证实例安全
static std::mutex m_mutex; // 互斥锁,用于加锁
Singleton() {} // 构造函数私有
~Singleton() {}
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed); // 先读取原子变量
std::atomic_thread_fence(std::memory_order_acquire); // 获取内存屏障,保证可见性
if (tmp == nullptr) { // 第一次检查
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) { // 第二次检查
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release); // 释放屏障,确保构造完成
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
};
// 初始化静态成员
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
实现步骤
1. 使用 std::atomic 存储 m_instance,确保在多线程环境下安全访问单例实例。
2. 使用 std::mutex 进行加锁,保证线程安全。
3. 双重检查锁定(DCLP):
3.1 第一次检查 if (tmp == nullptr),避免不必要的加锁。
3.2 第二次检查 if (tmp == nullptr),防止多个线程同时进入加锁区域导致创建多个实例。
4.使用 std::atomic_thread_fence:
4.1 memory_order_acquire 确保 tmp 读取到最新的数据。
4.2 memory_order_release 确保 m_instance 赋值完成后,其他线程能看到完整对象
- 更加现代的实现方式(C++11 及之后)
C++11 提供了更简单、安全的 std::call_once,可以替代 std::mutex 和 std::atomic。
#include <mutex>
class Singleton {
private:
Singleton() {} // 私有构造函数
~Singleton() {}
static std::once_flag initFlag; // 用于确保初始化只执行一次
static Singleton* instance;
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* getInstance() {
std::call_once(initFlag, []() { instance = new Singleton(); }); // 只执行一次
return instance;
}
};
// 初始化静态成员
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;
优势:
- std::call_once 比锁更高效,因为它只在 getInstance() 第一次调用时执行初始化。
- 避免了 std::mutex 的加锁开销,减少性能损耗。
- C++11 线程安全的懒汉式(推荐)
如果 C++11 及以上,可以直接使用局部静态变量(Meyers’ Singleton)。
class Singleton {
private:
Singleton() {}
~Singleton() {}
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
static Singleton instance; // 线程安全的懒汉式单例
return instance;
}
};
优势:
- 线程安全:C++11 标准保证了局部静态变量的初始化是线程安全的。
- 简单:没有 std::mutex 或 std::atomic,代码更易读。
- 生命周期管理:instance 会在程序结束时自动销毁。
典型调用方式
int main() {
Singleton* instance1 = Singleton::getInstance();
Singleton* instance2 = Singleton::getInstance();
if (instance1 == instance2) {
std::cout << "两个实例是相同的,单例模式成功!" << std::endl;
}
return 0;
}
总结
方式 | 线程安全 | 实现复杂度 | 适用场景 |
---|---|---|---|
传统懒汉式 | × 否 | 简单 | 仅限单线程 |
DCLP(双重检查锁定) | √ 是 | 复杂 | C++11 之前的多线程 |
std::atomic | √ 是 | 较复杂 | 需要高性能 |
std::call_once | √ 是 | 简单 | 现代 C++ 推荐 |
局部静态变量 | √ 是 | 最简单 | C++11 推荐 |
要点总结
Singleton模式中的实力构造器可以设置为protected以允许子类派生。
Singleton模式一般不要支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例,与Singleton模式的初衷违背。