std::call_once + 静态局部变量是C++11+线程安全单例的首选方案,因其由标准强制保证初始化原子性与顺序性,无需手动管理内存序、锁或原子操作,简洁、异常安全且编译器优化为无锁路径;裸指针加volatile必然失败,因volatile不提供跨线程同步语义,无法阻止“半构造对象”问题。

手写双重检查锁(DCLP)在 C++11+ 中极易出错,不推荐直接使用;真正安全、简洁、可移植的方案是 std::call_once + 静态局部变量。
为什么裸指针 + volatile 的 DCLP 必然失败
常见错误写法:static Singleton* instance = nullptr; 或加 volatile Singleton* instance。这两者在多线程下都可能返回“半构造对象”——即其他线程看到 instance 非空,但其成员变量尚未完成初始化。
根本原因不是编译器“优化过头”,而是 C++ 内存模型中:
- volatile 只禁编译器重排,不提供任何跨线程同步语义;
- new Singleton() 实际包含三步:分配内存 → 调用构造函数 → 写入指针值;
- CPU 和编译器都可能把第三步提前到第二步之前,导致指针发布早于构造完成。
主流编译器(GCC/Clang/MSVC)文档明确指出:volatile ≠ 线程安全。它无法替代 std::atomic。
std::atomic 是 DCLP 的底线要求,但仍有高风险
若坚持手写 DCLP,必须用 std::atomic<singleton></singleton>,且必须配对使用 memory order:
立即学习“C++免费学习笔记(深入)”;
- 第一次读(无锁路径):用
instance.load(std::memory_order_acquire),防止后续读被重排到它前面 - 构造后写入:用
instance.store(tmp, std::memory_order_release),防止构造动作被重排到它后面 - 第二次读(锁内)可用
load(std::memory_order_relaxed),因互斥量已提供顺序保证
漏掉任意一个 memory order,或误用 std::memory_order_seq_cst(性能差)、std::memory_order_consume(难正确),都会导致竞态。更麻烦的是:如果构造函数抛异常,store 不执行,但锁已释放,下次调用仍会重复尝试构造——需额外异常处理逻辑。
std::call_once + 静态局部变量才是实际工程首选
这是 C++11 标准规定的线程安全初始化机制,由编译器和运行时共同保障,无需手动管理原子性与内存序:
- 写法极简:
static Singleton instance;放在函数作用域内,直接return instance; - 首次调用自动触发初始化,且仅执行一次;后续调用零开销(无锁、无原子操作)
- 异常安全:若构造函数抛异常,下次调用会重试,不会留下未定义状态
- 主流编译器(GCC 5+/Clang 3.3+/MSVC 2015+)均已将其实现为“无锁 fast path + fallback 到
std::call_once”
它不依赖全局指针、不涉及裸内存管理、不暴露 new / delete 细节,天然规避了 DCLP 所有陷阱。
饿汉式 vs 懒汉式:别为了“懒加载”硬上 DCLP
饿汉式(静态全局对象)确实线程安全,但它是“急加载”——程序启动即构造,哪怕从不使用也会占用资源、延长启动时间,且析构时机不可控(静态对象析构顺序不确定)。
而 std::call_once 方案是真正的懒加载:首次访问才构造,且线程安全、析构可控(静态局部变量在首次构造后,会在程序退出时按逆序析构)。
如果你的单例构造开销极大(如加载大配置、建数据库连接),又必须懒加载,std::call_once 是唯一值得信任的选择;DCLP 不是性能优化手段,而是引入复杂性的风险源。


















