在C++中,单例模式(Singleton Pattern)是一种常用的设计模式,用来确保一个类只有一个实例,并提供全局访问点。实现线程安全的单例模式是一项挑战,尤其是在多线程环境下,需要保证实例在并发访问时不被多次创建。下面介绍几种常见的实现方式,并比较其优缺点。
1. 经典双重检查锁定(Double-Checked Locking, DCL)
class Singleton {
public:
static Singleton& getInstance() {
if (instance_ == nullptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex_);
if (instance_ == nullptr) { // 第二次检查
instance_ = new Singleton();
}
}
return *instance_;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
优点
- 只在第一次创建实例时获取锁,后续访问无需锁,性能较好。
缺点
- 需要使用
volatile或std::atomic以避免编译器优化导致的可见性问题(C++11之后,std::atomic已解决)。 - 代码相对繁琐,容易出错。
2. Meyers 单例(函数内部静态变量)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 线程安全的局部静态变量
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
优点
- 简单、易读。
- 自C++11起,局部静态变量的初始化是线程安全的,遵循”线程安全的初始化”(
call_once内部实现)。
缺点
- 不能延迟销毁:如果在
main函数退出时,单例的析构顺序与其他全局对象可能产生依赖。 - 对于极度受限的环境(如嵌入式)可能不适合。
3. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag_, [](){
instance_ = new Singleton();
});
return *instance_;
}
// 需要手动释放
static void destroy() {
delete instance_;
instance_ = nullptr;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
优点
- 明确控制实例的创建时机。
call_once只在第一次调用时执行初始化代码,后续调用不再锁。
缺点
- 需要手动释放内存,或者让实例成为
std::unique_ptr,否则会导致内存泄漏。 - 代码比Meyers略显繁琐。
4. 通过 std::shared_ptr 实现懒加载
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
std::call_once(initFlag_, [](){
instance_ = std::shared_ptr <Singleton>(new Singleton());
});
return instance_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::shared_ptr <Singleton> instance_;
static std::once_flag initFlag_;
};
std::shared_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
优点
- 采用智能指针管理生命周期,避免手动删除。
- 与多线程环境配合良好。
缺点
- 需要 C++11 标准库支持。
- 如果全局对象在程序结束时销毁,仍可能出现析构顺序问题。
5. 线程局部存储(Thread-Local Singleton)
如果你想为每个线程提供独立的单例实例,可使用 thread_local:
class ThreadSingleton {
public:
static ThreadSingleton& getInstance() {
thread_local ThreadSingleton instance;
return instance;
}
ThreadSingleton(const ThreadSingleton&) = delete;
ThreadSingleton& operator=(const ThreadSingleton&) = delete;
private:
ThreadSingleton() = default;
~ThreadSingleton() = default;
};
优点
- 每个线程有自己的实例,避免竞争。
- 初始化与销毁都在线程生命周期内完成。
缺点
- 不是真正意义上的“全局单例”,如果你需要全局唯一对象,这种方式不合适。
小结
- 最简洁:Meyers 单例(局部静态变量)是最推荐的实现方式,尤其在 C++11 之后。
- 可定制:如果你需要更细粒度的初始化控制,
std::call_once是更好的选择。 - 多线程安全:所有实现都在 C++11 标准库中使用原子操作和锁,确保线程安全。
在实际项目中,建议先尝试使用 Meyers 单例,除非有特殊需求(如需要显式销毁或跨模块的初始化顺序),再考虑其他实现方案。