在多线程环境下实现一个真正线程安全的单例是许多 C++ 开发者的常见挑战。虽然 C++11 以后提供了多种手段来保证线程安全,但细节处理不当仍可能导致 race condition 或性能瓶颈。本文从三方面剖析:懒汉式实现、双重检查锁以及Meyers 单例的优劣,并给出实际可用的代码示例。
1. 懒汉式(Lazy Singleton)+ 互斥量
class LazySingleton {
public:
static LazySingleton& getInstance() {
std::lock_guard<std::mutex> lock(mtx);
if (!instance) {
instance = new LazySingleton();
}
return *instance;
}
// 防止复制构造和赋值
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
~LazySingleton() { delete instance; }
private:
LazySingleton() {}
static LazySingleton* instance;
static std::mutex mtx;
};
LazySingleton* LazySingleton::instance = nullptr;
std::mutex LazySingleton::mtx;
优点:实现简单;对象按需创建。
缺点:每次获取实例都要加锁,导致高并发下性能受限。
2. 双重检查锁(Double-Checked Locking)
class DCLSingleton {
public:
static DCLSingleton& getInstance() {
DCLSingleton* tmp = instance.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new DCLSingleton();
instance.store(tmp, std::memory_order_release);
}
}
return *tmp;
}
// 同上,禁止拷贝
DCLSingleton(const DCLSingleton&) = delete;
DCLSingleton& operator=(const DCLSingleton&) = delete;
private:
DCLSingleton() {}
static std::atomic<DCLSingleton*> instance;
static std::mutex mtx;
};
std::atomic<DCLSingleton*> DCLSingleton::instance{nullptr};
std::mutex DCLSingleton::mtx;
优点:第一次实例化后后续访问无需加锁。
缺点:实现细节复杂,需要正确使用原子操作和内存序;若实现错误可能出现可见性问题。
3. Meyers 单例(静态局部变量)
class MeyersSingleton {
public:
static MeyersSingleton& getInstance() {
static MeyersSingleton instance; // C++11 之后的编译器保证线程安全
return instance;
}
MeyersSingleton(const MeyersSingleton&) = delete;
MeyersSingleton& operator=(const MeyersSingleton&) = delete;
private:
MeyersSingleton() {}
};
优点:最简洁,依赖编译器保证线程安全;没有额外的锁开销。
缺点:对象在程序结束前不会自动销毁(除非程序正常退出),并且如果构造函数抛异常,后续调用会再次尝试构造。
4. 需要注意的陷阱
-
静态初始化顺序问题
静态局部变量在第一次访问时初始化,避免了全局对象的初始化顺序不确定问题。但若单例依赖其他全局对象,仍需谨慎。 -
析构顺序
只在程序退出时析构;如果单例在析构前被其他全局对象使用,可能导致访问已释放内存。 -
跨 DLL 的单例
在 Windows DLL 里,每个进程或模块会有自己的静态存储,导致单例在不同模块中不共享。可以通过导出单例函数或使用全局共享内存来解决。 -
异常安全
双重检查锁和懒汉式在实例化期间若抛出异常,后续调用仍能安全重试;而 Meyers 单例若抛异常,C++ 标准规定该对象后续访问会再次尝试构造,直至成功。
5. 结论
- 推荐:使用 Meyers 单例,除非你需要对单例的销毁时机做精细控制。
- 如果需要懒加载:双重检查锁是一个折衷方案,但实现需要细心。
- 跨线程性能极致:若单例创建后对性能要求极高,考虑在程序启动阶段就初始化单例,避免运行时锁。
以上就是 C++ 线程安全单例实现的常见模式与注意点。希望能帮你在多线程项目中稳健地使用单例模式。