在多线程环境下,单例模式(Singleton)需要保证仅有一个实例且对所有线程可见。常见的实现方案包括“双重检查锁定(Double-Check Locking)”、使用C++11的局部静态变量,以及使用std::call_once。下面分别演示这三种方法,并讨论其优缺点。
1. 双重检查锁定(Double-Check Locking)
class Singleton {
public:
static Singleton* Instance() {
if (instance_ == nullptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex_);
if (instance_ == nullptr) { // 第二次检查
instance_ = new Singleton();
}
}
return instance_;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
说明
- 优点:实例创建延迟,且锁只在第一次创建时才生效。
- 缺点:在C++11之前,编译器对内存模型的优化可能导致“实例未完全初始化就被访问”的风险。C++11之后通过原子操作和内存屏障可以安全使用。
2. 局部静态变量(Meyer’s Singleton)
class Singleton {
public:
static Singleton& Instance() {
static Singleton instance; // C++11保证线程安全
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
说明
- 优点:代码最简洁,编译器自动保证线程安全。
- 缺点:无法延迟销毁(直到程序结束),不适合需要自定义销毁顺序的场景。
3. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& Instance() {
std::call_once(flag_, [](){ instance_ = new Singleton(); });
return *instance_;
}
~Singleton() { delete instance_; }
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance_;
static std::once_flag flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
说明
- 优点:显式控制初始化时机,兼顾延迟创建与线程安全。
- 缺点:需要手动管理内存,若不在适当位置调用
delete,可能导致泄漏。
4. 比较与选择
| 方法 | 延迟加载 | 线程安全保证 | 锁消耗 | 代码简洁性 | 适用场景 |
|---|---|---|---|---|---|
| 双重检查锁定 | ✅ | ✅(C++11后) | 低 | 较复杂 | 需要在老旧编译器下兼容 |
| 局部静态 | ✅ | ✅(C++11) | 0 | ✅ | 快速实现,销毁时机无关 |
call_once |
✅ | ✅ | 低 | 中等 | 需要自定义销毁,或与其他初始化逻辑配合 |
5. 实际应用示例
int main() {
auto& s1 = Singleton::Instance();
auto& s2 = Singleton::Instance();
// 两个引用指向同一个对象
assert(&s1 == &s2);
}
6. 小结
在C++11及以后,推荐使用局部静态变量实现单例,因为它既简洁又得到标准库的充分支持。若项目有特殊销毁顺序需求或想避免全局对象析构顺序问题,可以使用std::call_once。在需要兼容旧编译器时,可采用双重检查锁定,但需谨慎处理内存模型细节。