在多线程环境下,单例模式的实现不仅要保证全局唯一性,还必须防止竞争条件导致多实例创建。下面分别介绍几种常见的实现方式,比较它们的优缺点,并给出可直接使用的代码示例。
1. 基于局部静态变量(Meyer’s Singleton)
C++11 之后,函数内部的局部静态变量初始化是线程安全的。最简单、最推荐的实现方式:
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 线程安全的初始化
return instance;
}
// 禁止拷贝和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
优点:
- 代码简洁,几乎没有任何锁开销。
- 只在第一次调用时创建,随后直接返回。
缺点:
- 对象在程序结束时才销毁,若在程序退出时使用可能导致析构顺序问题(但大多数情况下可以忽略)。
2. std::call_once 与 std::once_flag
如果你想更明确地控制初始化过程,可以使用 std::call_once:
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, []() {
instancePtr = new Singleton();
});
return *instancePtr;
}
~Singleton() { delete instancePtr; }
private:
Singleton() = default;
static Singleton* instancePtr;
static std::once_flag initFlag;
};
Singleton* Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;
优点:
- 可以在初始化时执行更复杂的逻辑(如读取配置文件)。
- 与
Meyer's实现一样,线程安全。
缺点:
- 需要手动管理单例的销毁(
delete),否则可能出现内存泄漏。
3. 双重检查锁(Double-Checked Locking)
老式的做法,早期 C++ 标准中不保证线程安全,直到 C++11 的内存模型才可靠:
class Singleton {
public:
static Singleton* instance() {
Singleton* tmp = instancePtr.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instancePtr.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton();
instancePtr.store(tmp, std::memory_order_release);
}
}
return tmp;
}
private:
Singleton() = default;
static std::atomic<Singleton*> instancePtr;
static std::mutex mtx;
};
std::atomic<Singleton*> Singleton::instancePtr{nullptr};
std::mutex Singleton::mtx;
优点:
- 仅在第一次创建时使用锁,之后访问无需锁。
缺点:
- 代码复杂,容易出现错误。
- 需要严格遵守 C++11 的内存顺序规则。
4. 线程安全的懒加载(使用 std::shared_ptr)
如果你需要在单例中维护可变资源,并且想自动管理其生命周期,可以用 std::shared_ptr:
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::call_once(initFlag, []() {
ptr = std::make_shared <Singleton>();
});
return ptr;
}
private:
Singleton() = default;
static std::shared_ptr <Singleton> ptr;
static std::once_flag initFlag;
};
std::shared_ptr <Singleton> Singleton::ptr;
std::once_flag Singleton::initFlag;
优点:
- 自动析构,适合需要共享生命周期的场景。
- 对多线程读访问没有额外成本。
缺点:
- 每次访问返回一个
shared_ptr,虽然轻量,但仍有引用计数开销。
5. 何时选择哪种实现?
| 实现方式 | 代码简洁 | 初始化成本 | 资源释放 | 适用场景 |
|---|---|---|---|---|
| Meyer’s Singleton | ✅ | 仅一次 | 程序退出时 | 最常见、最简单 |
| std::call_once | ✅ | 仅一次 | 手动或显式释放 | 需要自定义初始化 |
| Double-Checked Locking | ❌ | 仅一次 | 手动 | 旧代码维护 |
| std::shared_ptr | ✅ | 仅一次 | 自动 | 资源共享、可变生命周期 |
在绝大多数现代 C++ 项目中,Meyer’s Singleton 是首选。它几乎无锁、易于使用,并且符合 C++11 之后的线程安全保证。除非你有特殊需求(如自定义析构、动态资源加载),否则不必使用更复杂的方案。
6. 小结
- 单例模式在多线程中实现时,核心是保证初始化阶段的线程安全。
- C++11 引入的局部静态变量和
std::call_once提供了最简洁且安全的实现方式。 - 传统的双重检查锁虽然可行,但更难维护,建议只在极其特殊的性能需求下使用。
- 记得处理好单例的析构顺序,避免在
atexit时出现依赖冲突。
通过以上方法,你可以在任何多线程 C++ 项目中安全、可靠地实现单例模式。