单例模式(Singleton)是一种常见的软件设计模式,用来保证某个类只有一个实例,并提供一个全局访问点。虽然实现单例在单线程环境中相对简单,但在多线程环境下,需要注意线程安全性,防止出现双重检查锁定(double-checked locking)导致的竞态条件。下面介绍几种在C++中实现线程安全单例的常用方法,并讨论各自的优缺点。
1. 采用局部静态变量(C++11之后)
C++11 引入了对局部静态变量的线程安全初始化保证。使用这种方式,最简洁且性能最优:
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 保证线程安全
return instance;
}
// 禁止复制和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
优点
- 代码简洁,几乎没有运行时开销。
- 通过语言层面保证线程安全,无需显式锁。
缺点
- 只在 C++11 及以后可用。
- 如果
Singleton的构造函数抛异常,随后再次访问instance()时会再次尝试构造。
2. 使用 std::call_once 与 std::once_flag
std::call_once 与 std::once_flag 是 C++11 提供的线程同步原语,专门用于一次性初始化:
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, [](){
instancePtr_ = new Singleton();
});
return *instancePtr_;
}
private:
Singleton() = default;
~Singleton() = default;
static std::once_flag initFlag_;
static Singleton* instancePtr_;
};
std::once_flag Singleton::initFlag_;
Singleton* Singleton::instancePtr_ = nullptr;
优点
- 明确表达“一次性初始化”的语义。
- 兼容旧版本编译器(只需 C++11)。
缺点
- 需要手动管理单例指针,易产生内存泄漏(虽然这里使用裸指针,但一般可用
std::unique_ptr)。 - 仍然有一点额外的同步开销(一次
std::call_once)。
3. 双重检查锁定(DCL)+ std::atomic
传统的双重检查锁定需要使用 std::atomic 或 std::volatile 以保证内存可见性。C++11 之后,可以这样写:
#include <atomic>
#include <mutex>
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() = default;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
优点
- 只在第一次创建时使用锁,之后访问几乎无锁。
缺点
- 代码复杂,易出错。
- 需要仔细使用内存顺序,错误的顺序会导致未定义行为。
- 在 C++11 前的编译器不支持
std::atomic,难以实现。
4. 静态局部变量 + 显式销毁(Meyers 单例)
如果你希望在程序结束时显式销毁单例,可以结合 std::unique_ptr:
class Singleton {
public:
static Singleton& instance() {
static std::unique_ptr <Singleton> ptr(new Singleton());
return *ptr;
}
// ...
private:
Singleton() = default;
};
优点
- 自动销毁,避免内存泄漏。
- 线程安全(C++11)。
缺点
- 仍然依赖 C++11 的线程安全初始化。
5. 何时使用哪种实现?
| 场景 | 推荐实现 |
|---|---|
| 简单项目,C++11+ | 局部静态变量(Meyers) |
| 需要手动管理生命周期或兼容旧编译器 | std::call_once |
| 性能极端敏感,且需要自定义内存管理 | DCL + std::atomic |
| 需要在多线程下进行自定义一次性初始化 | std::call_once |
6. 单例的陷阱
- 全局资源竞争:单例往往被过度使用,导致过多的全局共享状态,容易出现线程不安全。
- 测试难度:单例难以替换,单元测试时需使用全局状态来模拟,代码耦合度高。
- 资源释放顺序:如果单例持有其他全局资源,释放顺序需要仔细设计,避免悬空指针。
7. 结语
在 C++ 中实现线程安全单例,最推荐的做法是利用 C++11 的局部静态变量特性。它既简单又高效,同时无需手动同步,代码也更易维护。只有在特殊需求下才考虑使用 std::call_once 或双重检查锁定。无论选择哪种实现方式,都请确保对单例的使用场景有清晰的认识,避免滥用导致的并发难题。