饿汉式单例启动即初始化,天然线程安全;懒汉式首次调用才创建,需用std::call_once或双重检查锁定保障线程安全,但易出错,推荐优先使用饿汉式。
饿汉式在程序加载时就完成实例构造,后续所有调用都直接返回已创建的对象指针,不存在多线程竞争问题,无需加锁。
关键点在于 static 成员变量的初始化时机由编译器保证——C++11 起,static 局部变量的初始化是线程安全的;而静态成员变量(如类内定义的 static Instance*)在 main() 执行前完成,且仅一次。
常见错误是把指针声明和 new 拆开写,导致非原子操作:
class Singleton {
private:
static Singleton* instance;
Singleton() = default; // 防止外部构造
public:
static Singleton* getInstance() {
return instance; // ❌ instance 可能为 nullptr 或未初始化
}
};
Singleton* Singleton::instance = new Singleton(); // ✅ 此行才真正构造更推荐写法(C++11+):
立即学习“C++免费学习笔记(深入)”;
class Singleton {
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& getInstance() {
static Singleton instance; // ✅ 局部静态变量,线程安全初始化
return instance;
}
};main 结束后按逆序销毁,若其他静态对象依赖它,可能访问已析构对象懒汉式延迟资源占用,但 getInstance() 中的判空 + 构造逻辑不是原子操作,多线程下极易出现重复 new 或返回未完全构造的对象。
典型错误写法(双重检查锁定漏锁):
static Singleton* getInstance() {
if (instance == nullptr) { // 第一次检查
instance = new Singleton(); // ❌ 构造+赋值非原子,可能被重排,其他线程看到半初始化对象
}
return instance;
}正确实现(C++11 double-checked locking pattern):
class Singleton {
private:
static std::atomic instance;
static std::mutex mtx;
Singleton() = default;
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard lock(mtx);
tmp =
instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton();
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
};
std::atomic Singleton::instance{nullptr};
std::mutex Singleton::mtx; std::atomic 替代裸指针,否则无法防止指令重排memory_order_acquire 和 memory_order_release 保证构造完成后再对其他线程可见std::call_once
相比手写 DCLP,std::call_once 更简洁、不易出错,且由标准库保证绝对只执行一次。
class Singleton {
private:
static Singleton* instance;
static std::once_flag init_flag;
Singleton() = default;
public:
static Singleton* getInstance() {
std::call_once(init_flag, []() {
instance = new Singleton();
});
return instance;
}
};
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::init_flag;std::call_once 内部已做线程同步,无需额外锁或原子操作instance 仍需声明为 static,且不能在 lambda 外提前使用std::unique_ptr 包裹并注册 atexit),否则内存泄漏饿汉式看似“浪费”,实则规避了绝大多数线程安全陷阱;懒汉式看似灵活,却把复杂性推给了开发者。
真实项目中容易被忽略的点:
std::call_once,其内部实现依赖 OS 级同步原语,在极低概率下(如 fork 后)可能异常,但绝大多数场景可忽略constinit,但目前对单例帮助有限,仍无法解决跨编译单元初始化顺序问题除非明确知道构造开销极大、且确定不会引发静态初始化依赖,否则默认用饿汉式(局部静态变量版本)最省心。