在多线程环境下,单例(Singleton)模式需要保证只有一个实例,并且在所有线程中都能安全访问。以下几种实现方式在 C++17 及以上标准下都能保证线程安全且具有良好的性能。
1. Meyer’s Singleton(局部静态对象)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 起保证线程安全的初始化
return instance;
}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
void do_something() { /* ... */ }
private:
Singleton() {} // 私有构造函数
};
优点
- 简单直观
- C++11 起局部静态对象的初始化是线程安全的(即使多个线程同时调用
instance(),也只会初始化一次)。 - 对象在程序结束时自动析构,无需手动管理。
缺点
- 如果需要在程序结束前显式销毁单例,需额外实现。例如使用
std::unique_ptr并配合std::atexit。
2. 双重检查锁(Double-Checked Locking)
class Singleton {
public:
static Singleton* instance() {
if (!ptr_) { // 第一次检查
std::lock_guard<std::mutex> lock(mtx_);
if (!ptr_) { // 第二次检查
ptr_ = new Singleton();
}
}
return ptr_;
}
~Singleton() { delete ptr_; }
private:
Singleton() {}
static Singleton* ptr_;
static std::mutex mtx_;
};
Singleton* Singleton::ptr_ = nullptr;
std::mutex Singleton::mtx_;
优点
- 只在第一次创建实例时锁定,后续访问不再涉及锁。
缺点
- 需要手动销毁单例(如在
atexit里调用),否则可能导致内存泄漏。 - 代码稍显繁琐,且如果没有使用
volatile或 C++11 的内存模型,可能存在重排序导致的 UB。
3. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, [](){ ptr_ = new Singleton(); });
return *ptr_;
}
private:
Singleton() {}
static Singleton* ptr_;
static std::once_flag flag_;
};
Singleton* Singleton::ptr_ = nullptr;
std::once_flag Singleton::flag_;
优点
std::call_once保证只调用一次初始化回调,内部已实现线程安全。- 代码更简洁,且不需要手动锁。
缺点
- 需要手动销毁单例(可在
atexit注册delete ptr_;)。
4. 静态智能指针(现代 C++ 推荐)
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
static std::shared_ptr <Singleton> ptr(new Singleton());
return ptr;
}
private:
Singleton() {}
};
优点
- 通过
std::shared_ptr自动管理生命周期。 - 线程安全且可在多处共享实例。
缺点
- 每次访问返回
std::shared_ptr,可能产生不必要的引用计数开销。
小结
- 对于绝大多数场景,Meyer’s Singleton(局部静态对象)是最简洁且线程安全的实现。
- 如需显式控制实例创建时间或销毁顺序,可考虑
std::call_once或std::once_flag。 - 双重检查锁虽然在某些语言/编译器中可行,但在 C++11 之后
std::call_once更安全、更简洁。
技巧提示
- 禁止拷贝构造/赋值:单例不应被拷贝。
- 懒初始化:仅在首次使用时才创建实例,避免不必要的开销。
- 资源释放:若单例持有外部资源(文件句柄、网络连接等),请在程序退出前显式释放或使用 RAII 包装。
通过上述实现方法,您可以根据项目需求选择最适合的线程安全单例实现,既保证性能,又确保代码的可维护性。