饿汉式单例启动即初始化,天然线程安全;懒汉式首次调用才创建,需用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保证构造完成后再对其他线程可见 - 两次判空缺一不可:第一次避免无谓加锁,第二次防止加锁后已被其他线程创建
- 不建议手写 DCLP —— 容易出错,优先用局部静态变量(饿汉式)或
std::call_once
更现代的懒汉式替代:std::call_once + once_flag
相比手写 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),否则内存泄漏
饿汉式 vs 懒汉式:选型关键看初始化成本与依赖关系
饿汉式看似“浪费”,实则规避了绝大多数线程安全陷阱;懒汉式看似灵活,却把复杂性推给了开发者。
真实项目中容易被忽略的点:
- 如果单例构造函数中调用了其他尚未初始化的全局对象(比如另一个单例),饿汉式可能因静态初始化顺序未定义而崩溃
- 懒汉式若用
std::call_once,其内部实现依赖 OS 级同步原语,在极低概率下(如 fork 后)可能异常,但绝大多数场景可忽略 - C++20 引入
constinit,但目前对单例帮助有限,仍无法解决跨编译单元初始化顺序问题 - 真正需要懒汉式的场景极少——多数所谓“耗资源”其实是错觉,真正瓶颈往往在 I/O 或网络,而非内存分配
除非明确知道构造开销极大、且确定不会引发静态初始化依赖,否则默认用饿汉式(局部静态变量版本)最省心。











