在现代C++中,实现线程安全的单例模式并不需要过于复杂的同步机制。随着C++11引入了对线程的标准支持以及对局部静态变量初始化的线程安全保证,我们可以利用这些特性来写出简洁、可靠的单例。下面将从多个角度展开,帮助你深入理解实现过程、常见陷阱以及优化技巧。
1. 单例的基本需求
单例模式的核心目标是:
- 全局唯一性:同一进程内只能存在一个实例。
- 按需创建:第一次使用时才实例化,之后复用同一对象。
- 可销毁:在程序结束或需要清理时,能够安全销毁实例。
在多线程环境下,关键是保证实例化过程的互斥。如果两个线程同时检测到实例为空并尝试创建,可能导致双重实例或破坏对象状态。
2. C++11 方式:局部静态变量
C++11 规定:对局部静态变量的初始化是 线程安全 的。也就是说,第一次访问时,编译器会自动插入必要的锁,保证只有一个线程完成初始化。
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 线程安全的局部静态
return instance;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
void doSomething() { /*...*/ }
private:
Singleton() { /* 可能耗时的初始化 */ }
};
关键点
- 私有构造函数:防止外部直接实例化。
- 删除拷贝/赋值:保证唯一性。
- 静态局部:保证第一次进入
getInstance()时才实例化,且线程安全。 - 返回引用:避免不必要的拷贝。
优点
- 实现简洁:无需显式锁。
- 延迟初始化:真正需要时才创建。
- 销毁顺序:程序结束时自动销毁,顺序符合
static规则。
缺点
- 无法自定义销毁时机:如果你需要在特定时刻销毁实例,局部静态不适合。
- 可能的死锁:如果构造函数内部使用了同一静态对象(递归初始化),会导致死锁。
3. 传统双重检查锁(DCL)
如果你必须在 C++11 之前的标准(C++03)或想对销毁时机进行更细粒度控制,可以使用双重检查锁(Double-Check Locking)模式。需要注意的是,这种实现依赖于硬件支持原子操作以及内存屏障。以下是常见的实现:
class Singleton {
public:
static Singleton* getInstance() {
if (!instance_) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) { // 第二次检查
instance_ = new Singleton();
}
}
return instance_;
}
static void destroy() {
std::lock_guard<std::mutex> lock(mutex_);
delete instance_;
instance_ = nullptr;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
void doSomething() { /*...*/ }
private:
Singleton() { /*...*/ }
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
注意事项
- 内存屏障:在旧编译器/平台上可能需要显式使用
volatile或原子指针,避免指令重排导致的脏读。 - 单例销毁:手动销毁时要确保没有线程正在使用实例,否则可能产生悬空指针。
4. 静态局部与 std::call_once
C++11 还提供了 std::call_once 和 std::once_flag,用于手动控制一次性初始化,且支持在任何函数中使用:
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(flag_, []() {
instance_ = new Singleton();
});
return *instance_;
}
// 其它成员...
private:
static Singleton* instance_;
static std::once_flag flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
优点:
- 更灵活:可以在任何作用域内调用
call_once,而不需要局部静态。 - 适用于单例与其他一次性初始化:可以共用同一机制。
5. 线程安全与资源管理
无论采用哪种实现,单例对象往往管理着全局资源(数据库连接、日志系统、线程池等)。在多线程环境中,需要:
- 内部同步:单例内部的成员函数若涉及共享状态,必须自行加锁或使用原子类型。
- 内存可见性:确保所有线程对对象状态的读写都能正确同步。
- 生命周期:避免在单例销毁后仍有线程访问,导致未定义行为。
6. 进阶:多继承与可配置单例
如果单例需要支持多种实现(如插件化的日志记录器),可以把单例设计为基类,子类通过 `getInstance
()` 方式创建: “`cpp class Logger { public: static Logger& getInstance() { static Logger instance; return instance; } virtual void log(const std::string& msg) = 0; }; class FileLogger : public Logger { public: void log(const std::string& msg) override { /* 写文件 */ } }; class ConsoleLogger : public Logger { public: void log(const std::string& msg) override { std::cout