单例模式(Singleton Pattern)是一种常见的设计模式,用来保证一个类只有一个实例,并提供全局访问点。在多线程环境下,如何确保单例实例的线程安全成为了开发者面临的关键问题。本文将从 C++11 及之后的标准出发,介绍几种实现线程安全单例的方法,并对比它们的优缺点,帮助你在项目中做出合适的选择。
1. 采用 std::call_once 与 std::once_flag
std::call_once 是 C++11 提供的原子性调用机制,只会执行一次闭包函数。配合 std::once_flag,可以在多线程环境下安全地初始化单例。
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, []() { instancePtr_ = new Singleton; });
return *instancePtr_;
}
// 其他公共成员
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instancePtr_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instancePtr_ = nullptr;
std::once_flag Singleton::initFlag_;
优点
- 代码简洁,逻辑清晰。
- 线程安全性由标准库保证,无需手动同步。
- 延迟初始化(只在首次调用
instance()时创建)。
缺点
- 对
new的调用未做异常处理(若构造函数抛异常会导致单例永不创建)。 - 对于极端高性能要求的场景,
std::call_once仍有一定开销。
2. 局部静态变量(C++11 后的静态局部初始化)
自 C++11 起,局部静态变量的初始化是线程安全的。使用这种方式可以进一步简化代码:
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11保证线程安全
return instance;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点
- 代码最简洁。
- 编译器保证线程安全,性能优异。
- 内存管理完全由栈/静态存储控制,避免了手动
new/delete。
缺点
- 只能使用栈/静态存储,若单例需要在堆上存放大量资源或具有复杂生命周期管理时不太合适。
- 需要在 C++11 及之后编译器支持。
3. 双重检查锁(Double-Check Locking)
这是早期多线程单例常用的模式,利用 std::mutex 对实例进行双重检查。注意,必须使用 std::atomic 或内存序(memory ordering)来保证可见性。
#include <mutex>
#include <atomic>
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_;
优点
- 只在首次实例化时使用互斥锁,后续调用不需要锁,性能相对较好。
- 兼容 C++03 及以前的编译器(如果用
pthread需要自行处理内存序)。
缺点
- 代码相对复杂,容易出现错误。
- 需要手动管理
new/delete,易导致泄漏。 - 现代编译器对局部静态变量的实现已优化,通常不需要此模式。
4. 采用智能指针(std::shared_ptr 或 std::unique_ptr)
如果单例需要在程序结束时自动销毁,或者想利用智能指针的安全特性,可以这样写:
#include <memory>
#include <mutex>
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::call_once(initFlag_, []() {
instance_ = std::shared_ptr <Singleton>(new Singleton);
});
return instance_;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::shared_ptr <Singleton> instance_;
static std::once_flag initFlag_;
};
std::shared_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
优点
- 自动内存管理,避免泄漏。
- 可在程序中多处获取共享实例,符合
shared_ptr的语义。
缺点
- 需要
std::shared_ptr的引用计数开销。 - 如果想让单例始终只存在一个实例,使用
unique_ptr并在静态内部返回引用可能更合适。
5. 对比总结
| 方法 | 线程安全 | 代码简洁度 | 兼容性 | 内存管理 | 适用场景 |
|---|---|---|---|---|---|
std::call_once |
✔ | 中等 | C++11+ | 手动(new) | 需要自定义构造逻辑 |
| 局部静态 | ✔ | 极简 | C++11+ | 自动 | 资源不需要堆分配 |
| 双重检查锁 | ✔ | 复杂 | C++03+ | 手动 | 兼容老编译器 |
| 智能指针 | ✔ | 中等 | C++11+ | 自动 | 需要自动销毁或共享 |
在大多数现代 C++ 项目中,局部静态变量 是最推荐的实现方式,因为它既简洁又可靠。若你在使用 C++03 或想要更细粒度的初始化控制,std::call_once 或双重检查锁都是可行的选择。若单例对象在程序结束时需要显式销毁,考虑使用智能指针包装。
6. 小结
线程安全的单例在多线程 C++ 应用中经常被使用,关键在于选择合适的实现方式。掌握 std::call_once、局部静态变量以及双重检查锁的原理与使用场景,可以让你在项目中更灵活、稳健地使用单例模式。无论采用哪种方法,记得在构造函数里抛出异常的情况下做好回收或重试机制,避免单例永远无法创建。祝你编码愉快!