在多线程环境下,单例模式需要保证只有一份实例,并且在并发访问时不会出现竞争条件。C++11 起,标准库提供了许多工具可以帮助我们轻松实现线程安全的单例。下面分别介绍几种常用实现方式,并讨论它们的优缺点。
1. Meyers 单例(局部静态对象)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后保证线程安全
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() { /* 初始化 */ }
};
-
优点
- 代码最简洁,几乎无维护成本。
static对象的初始化在第一次调用instance()时完成,延迟加载。- C++11 之后,编译器保证局部静态对象的初始化是线程安全的。
-
缺点
- 不能延迟销毁:在程序退出时才会被销毁,若需要提前销毁或自定义销毁顺序则无法满足。
- 若构造函数抛异常,后续调用会再次尝试初始化,导致异常传播。
2. 双重检查锁(Double-Check Locking)
class Singleton {
public:
static Singleton* instance() {
Singleton* tmp = instance_;
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance_;
if (!tmp) {
tmp = new Singleton();
instance_ = tmp;
}
}
return tmp;
}
// 其他接口...
private:
Singleton() { /* 初始化 */ }
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
-
优点
- 只在首次创建实例时加锁,后续访问高效。
-
缺点
- 需要手动管理内存(需要在合适时机
delete)。 - 代码稍显复杂,容易出现错误。
- 在某些编译器/平台下,双重检查锁存在可见性问题,除非使用
std::atomic或memory_order。
- 需要手动管理内存(需要在合适时机
3. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, []() { instance_ = new Singleton(); });
return *instance_;
}
// 防止拷贝与赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() { /* 初始化 */ }
static Singleton* instance_;
static std::once_flag flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
-
优点
- 线程安全且只锁一次,后续访问无需加锁。
- 对
once_flag的使用比手动mutex更加安全,避免忘记解锁。
-
缺点
- 需要手动删除
instance_(可通过std::atexit注册delete)。 - 代码略长,但更具可读性。
- 需要手动删除
4. 智能指针 + std::call_once
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, []() {
instance_ = std::unique_ptr <Singleton>(new Singleton());
});
return *instance_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() { /* 初始化 */ }
static std::unique_ptr <Singleton> instance_;
static std::once_flag flag_;
};
std::unique_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
-
优点
- 自动管理内存,程序退出时会自动销毁。
- 与
call_once配合使用,确保初始化只执行一次。
-
缺点
- 在多线程环境下,某些编译器仍然建议显式地使用
std::mutex来保护对instance_的访问,尽管unique_ptr本身不是线程安全的。
- 在多线程环境下,某些编译器仍然建议显式地使用
5. 对象销毁顺序
在 C++ 中,静态对象的销毁顺序在不同翻译单元之间未定义。若单例使用局部静态对象(Meyers 单例),在 atexit 阶段它会被自动销毁,且销毁顺序是逆序。因此,若单例被其他静态对象使用,可能导致“静态销毁顺序问题”。
解决办法
- 将单例的生命周期控制在程序的主要入口(如
main)内,手动销毁。 - 或者使用
std::unique_ptr与std::atexit注册销毁函数。
class Singleton {
public:
static Singleton& instance() {
static Singleton instance;
return instance;
}
// ...
};
int main() {
// 通过 Singleton::instance() 使用单例
// 程序结束时自动销毁
return 0;
}
6. 性能与可维护性权衡
- Meyers 单例:最简洁,推荐首选。
std::call_once:兼顾线程安全与可维护性,推荐在需要手动内存管理时使用。- 双重检查锁:若对性能极致敏感,可使用,但代码更复杂,风险更高。
7. 小结
-
选择合适的实现
- 若只需线程安全且简单,直接使用 Meyers 单例即可。
- 若需要显式控制销毁或更复杂的初始化,
std::call_once是更安全的选项。
-
避免共享可变状态
- 单例往往成为全局状态的集中点,务必谨慎使用,避免产生“全局状态”危害。
-
测试并发
- 在多线程测试中,确保单例在高并发下不会产生多实例。
通过以上方法,你可以在 C++11 及以后版本中实现高效且安全的单例模式,避免传统实现带来的多线程竞态与内存泄漏等问题。祝编码愉快!