在现代 C++(C++11 及以后)中,实现线程安全的单例模式其实比以往更为简洁。核心思路是利用 局部静态变量 的特性——其初始化是线程安全的,并且只会在第一次进入作用域时执行一次。下面我们从头到尾展示一个完整的实现,并剖析其内部细节、性能考量以及常见误区。
1. 传统单例实现回顾
class ClassicSingleton {
public:
static ClassicSingleton& instance() {
if (!m_instance) {
m_instance = new ClassicSingleton();
}
return *m_instance;
}
// 其他业务方法...
private:
ClassicSingleton() {}
ClassicSingleton(const ClassicSingleton&) = delete;
ClassicSingleton& operator=(const ClassicSingleton&) = delete;
static ClassicSingleton* m_instance;
};
ClassicSingleton* ClassicSingleton::m_instance = nullptr;
这个实现存在几个明显问题:
- 线程安全:
m_instance的懒加载不是原子操作,多个线程可能同时创建实例。 - 内存泄漏:没有显式释放
m_instance,程序退出时才会自动释放,或者需要在atexit中手动释放。 - 性能:每次访问
instance()都要检查指针是否为空,且new操作带来不必要的开销。
2. C++11 局部静态变量实现
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 线程安全的局部静态变量
return instance;
}
// 示例业务方法
void do_something() { std::cout << "Doing something\n"; }
private:
Singleton() { std::cout << "Singleton constructed\n"; }
~Singleton() { std::cout << "Singleton destroyed\n"; }
// 禁止拷贝与赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
关键点说明
-
线程安全的初始化
C++11 标准保证局部静态变量的初始化是 按需、线程安全的。编译器会自动插入必要的锁或使用原子操作,确保即使多个线程同时调用instance(),也只会构造一次Singleton。 -
无内存泄漏
该对象的生命周期与程序相同:构造时在栈上分配,程序结束时自动销毁,避免手动delete的麻烦。 -
性能优势
第一次调用时的初始化成本不算大,后续调用仅返回已有对象,无需额外锁或判断。由于初始化仅发生一次,开销极小。
3. 进一步优化:懒加载 vs 预加载
- 懒加载(上面示例):只在第一次真正使用时才创建。适合实例化成本高或不确定是否会使用的单例。
- 预加载:在程序启动时就创建单例,避免后续多线程竞争导致的短暂停顿。
class PreloadSingleton {
public:
static PreloadSingleton& instance() {
return *s_instance;
}
private:
PreloadSingleton() { std::cout << "Preloaded\n"; }
~PreloadSingleton() {}
static PreloadSingleton* s_instance;
friend void init_preload(); // 由外部函数初始化
};
PreloadSingleton* PreloadSingleton::s_instance = nullptr;
void init_preload() {
PreloadSingleton::s_instance = new PreloadSingleton();
}
调用 init_preload() 可以放在 main() 或 DllMain 等入口点,保证单例提前创建。
4. 常见误区
-
错误的宏替换
许多人用#define SINGLETON(...)宏来创建单例,但宏不具备类型安全和作用域控制,容易导致名字冲突。 -
忘记
delete关键字
在 C++11 之前,常用static Singleton* instance;并手动delete,容易出现野指针或多次删除。 -
忽视异常安全
如果构造函数抛异常,局部静态变量会被销毁并重新尝试构造,直到成功为止。若不想重复构造,可捕获异常并手动控制。
5. 适用场景举例
- 日志系统:全局唯一日志记录器,避免多实例写冲突。
- 配置管理:一次读取配置文件,随后所有模块共享同一实例。
- 线程池:全局线程池实例,避免多线程创建不必要的线程。
6. 小结
- C++11 之后,使用局部静态变量即可实现线程安全的单例,代码简洁且性能优秀。
- 对于特殊需求(提前创建、异常处理)可以结合手动初始化或自定义锁实现。
- 牢记禁止拷贝与赋值,保持单例唯一性。
单例模式在 C++ 中既是设计模式的经典,也展示了语言标准如何在细节层面帮助我们写出安全、简洁的代码。只要把握好上述要点,即可在项目中自如运用。