在 C++ 中实现单例模式时,最常见的挑战之一就是保证在多线程环境下的线程安全。下面将介绍几种常用的实现方式,并比较它们的优缺点。
1. Meyers 单例(局部静态变量)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 起保证线程安全
return instance;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
- 优点:代码简洁,依赖标准库实现,默认线程安全(C++11 起)。
- 缺点:无法控制实例的销毁时机;在程序退出时,可能导致顺序不确定的析构顺序问题。
2. 双重检查锁(Double-Checked Locking)
class Singleton {
public:
static Singleton* getInstance() {
if (!instance) {
std::lock_guard<std::mutex> lock(mutex);
if (!instance) {
instance = new Singleton();
}
}
return instance;
}
~Singleton() { delete instance; }
private:
Singleton() = default;
static Singleton* instance;
static std::mutex mutex;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
- 优点:显式控制实例创建时机,支持延迟初始化。
- 缺点:实现错误多(尤其是
volatile的使用),在 C++11 之前编译器的优化可能导致实例创建不安全。
3. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(flag, []() { instance.reset(new Singleton()); });
return *instance;
}
private:
Singleton() = default;
static std::unique_ptr <Singleton> instance;
static std::once_flag flag;
};
std::unique_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::flag;
- 优点:标准化、跨平台,保证线程安全,延迟初始化。
- 缺点:需要 C++11,若对销毁时机有严格要求,可能需要手动销毁。
4. 模板化单例(多类型单例)
template<typename T>
class Singleton {
public:
static T& getInstance() {
static T instance;
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
使用示例:
struct Config { /* ... */ };
Config& cfg = Singleton <Config>::getInstance();
- 优点:可为不同类型提供独立的单例实例。
- 缺点:与 Meyers 单例相同,销毁顺序不确定。
5. 资源泄漏与防止手动删除
单例通常在程序整个生命周期内存在,手动删除实例可能会导致程序在退出时产生错误。可以使用 std::shared_ptr 并让其在程序结束时自动释放,或者使用 std::atexit 注册销毁函数。
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
std::atexit(&Singleton::destroy);
return instance;
}
private:
Singleton() = default;
static void destroy() {
// 进行必要的清理工作
}
};
6. 性能比较
| 方法 | 延迟初始化 | 线程安全保证 | 开销 |
|---|---|---|---|
| Meyers | ✅ | ✅(C++11) | 极低 |
| 双重检查 | ✅ | ✅(需细心) | 中等 |
call_once |
✅ | ✅ | 中等 |
| 模板化 | ✅ | ✅(C++11) | 极低 |
在实际项目中,推荐使用 Meyers 单例 或 std::call_once 作为首选,因为它们既简洁又可靠。只有在需要手动控制销毁顺序或多类型单例时,才考虑使用模板化或双重检查锁。
7. 小结
- Meyers 单例:最简单,C++11 之后线程安全,适合大多数场景。
std::call_once:标准化、延迟初始化,适用于需要更细粒度控制时。- 双重检查锁:在旧代码基中可能见到,但实现细节复杂,易出错。
- 模板化单例:满足多类型单例需求,但销毁顺序仍需注意。
通过掌握这些实现方式,开发者可以在 C++ 程序中安全、高效地使用单例模式。