单例模式(Singleton Pattern)是设计模式中的一种常见用法,它保证一个类只有一个实例,并提供全局访问点。在多线程环境下,若不加以控制,多个线程可能同时创建实例,导致产生多个对象,破坏单例的核心特性。本文将介绍几种在 C++11 及更高版本中实现线程安全单例的方法,并对每种实现的优缺点进行比较。
1. 局部静态变量(Meyers Singleton)
class Singleton {
public:
static Singleton& instance() {
static Singleton inst; // C++11 保证线程安全
return inst;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
-
优点
- 简单易读,代码最短。
- C++11 规定局部静态变量的初始化是线程安全的。
- 无需手动加锁,避免了死锁风险。
-
缺点
- 对象的生命周期与程序的生命周期相同,无法控制销毁顺序,可能导致“静态销毁顺序问题”。
- 需要 C++11 或更高版本的编译器。
2. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, [](){ instance_.reset(new Singleton); });
return *instance_;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::unique_ptr <Singleton> instance_;
static std::once_flag flag_;
};
std::unique_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
-
优点
- 明确控制实例创建时机,避免了局部静态变量的静态销毁问题。
- 适用于需要延迟初始化或在特定时间点销毁的场景。
-
缺点
- 代码略显繁琐。
unique_ptr需要手动管理生命周期,若需要手动销毁,需自行实现。
3. 原子指针 + 双重检查锁定(Double-Check Locking)
class Singleton {
public:
static Singleton* instance() {
Singleton* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton;
instance_.store(tmp, std::memory_order_release);
}
}
return tmp;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
-
优点
- 适用于需要手动销毁单例或自定义内存分配策略的场景。
- 对多线程性能友好:首次调用时有锁,后续访问无锁。
-
缺点
- 需要对原子操作和内存序进行严格理解,易出现细微错误。
- 代码相对复杂,易维护成本高。
4. 静态局部对象 + 析构函数优先级控制
如果需要在程序退出时保证单例先于其他静态对象析构,可以使用 std::shared_ptr 与 std::weak_ptr 结合 std::atexit 注册:
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
static std::weak_ptr <Singleton> weak;
std::shared_ptr <Singleton> shared = weak.lock();
if (!shared) {
shared = std::shared_ptr <Singleton>(new Singleton);
weak = shared;
std::atexit([](){ /* 自定义销毁逻辑 */ });
}
return shared;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
-
优点
- 可在
atexit里执行更复杂的销毁逻辑。 - 利用
shared_ptr自动管理生命周期。
- 可在
-
缺点
- 需要手动注册
atexit,可能导致注册顺序不确定。 - 代码仍然较长。
- 需要手动注册
5. 哪个方案最合适?
| 方案 | 适用场景 | 复杂度 | 线程安全 | 生命周期控制 |
|---|---|---|---|---|
| Meyers | 简单快速 | 低 | 兼容 C++11 | 受静态销毁顺序限制 |
std::call_once |
需要延迟初始化或手动销毁 | 中 | 兼容 C++11 | 可控制 |
| 双重检查锁定 | 需要手动销毁或自定义分配 | 高 | 需要细心 | 可控制 |
std::atexit+shared_ptr |
复杂销毁逻辑 | 高 | 兼容 C++11 | 可控制 |
对于大多数现代 C++ 项目,Meyers 单例 已经足够安全且代码最简洁;如果你担心静态销毁顺序或需要在程序退出时执行特定操作,建议使用 std::call_once 或 std::atexit 方案。若项目要求极高的性能并且你熟悉原子操作,双重检查锁定仍然是一个值得考虑的选项。
6. 常见错误与调试技巧
-
未加锁的多线程实例化
- 结果:多个实例被创建,导致单例失效。
- 解决:使用
std::call_once或局部静态变量。
-
构造函数抛异常
- 对于 Meyers 单例,异常会导致后续访问失败。
- 建议在构造函数内部捕获异常并记录错误,或使用
try-catch包裹instance()调用。
-
静态销毁顺序问题
- 当单例在其他静态对象析构期间被访问,可能导致崩溃。
- 解决:使用
std::call_once+std::unique_ptr或std::atexit注册销毁顺序。
-
可见性问题
- 在双重检查锁定实现中,必须使用
std::memory_order_acquire/release以保证内存可见性。 - 避免使用
std::relaxed,除非你完全理解其后果。
- 在双重检查锁定实现中,必须使用
7. 结语
线程安全的单例在 C++ 开发中依然是一种重要模式,尤其是在大型项目中需要共享资源时。现代 C++ 标准为我们提供了多种成熟的实现方式,从最简洁的局部静态变量到细粒度的原子操作。根据项目需求、编译器版本以及对生命周期控制的严格程度,选择合适的实现方案,可以在保证线程安全的同时,保持代码的简洁与可维护性。