单例模式的核心在于确保一个类只有一个实例,并提供全局访问点。随着 C++11 及其后的标准引入了多线程支持,传统的单例实现方式(如饿汉式、懒汉式)需要额外的同步机制来保证线程安全。下面介绍几种常见且高效的实现方法,并对比其优缺点。
1. 局部静态变量(Meyers Singleton)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 guarantees thread-safe initialization
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
- 优点:实现最简洁,编译器负责线程安全。无显式锁,性能优良。
- 缺点:如果你想在程序结束前显式销毁单例,需要自定义
std::atexit或使用std::unique_ptr包装。
2. 带双重检查锁(Double-Checked Locking)
class Singleton {
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton();
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
// 同样禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::atomic<Singleton*> instance;
static std::mutex mtx;
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;
- 优点:延迟初始化,且线程安全。
- 缺点:实现相对复杂,易出错。现代编译器和标准库的实现已经足够优雅,通常不需要手动实现。
3. 显式销毁的懒汉式(使用 std::unique_ptr)
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag, []{
instance.reset(new Singleton);
});
return *instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::unique_ptr <Singleton> instance;
static std::once_flag initFlag;
};
std::unique_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::initFlag;
- 优点:支持在程序退出时显式销毁,避免懒汉式带来的“野指针”问题。
- 缺点:需要 `#include `、“,但实现仍然较为简洁。
4. 静态局部对象与 std::shared_ptr(可定制生命周期)
如果你想让单例对象在多处持有引用,可以结合 std::shared_ptr:
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
static std::shared_ptr <Singleton> ptr(new Singleton, [](Singleton* p){ delete p; });
return ptr;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
- 优点:可以通过引用计数控制对象的生命周期,适用于需要在多线程环境中共享实例的场景。
- 缺点:若不小心产生循环引用,可能导致资源泄漏。
5. 线程安全的双向单例(双重初始化 + 读写锁)
在高并发读多写少的场景下,读写锁可以提升性能:
class Singleton {
public:
static Singleton* getInstance() {
std::shared_lock<std::shared_mutex> rlock(rwMutex);
if (!instance) {
rlock.unlock();
std::unique_lock<std::shared_mutex> wlock(rwMutex);
if (!instance) {
instance = new Singleton;
}
rlock.lock();
}
return instance;
}
private:
static Singleton* instance;
static std::shared_mutex rwMutex;
};
Singleton* Singleton::instance = nullptr;
std::shared_mutex Singleton::rwMutex;
- 优点:多线程读操作不阻塞,写操作仍然保证安全。
- 缺点:实现较为复杂,且在实例化后不再需要写锁,可能导致不必要的锁开销。
何时选择哪种实现?
| 实现方式 | 适用场景 | 主要优势 | 主要缺点 |
|---|---|---|---|
| 局部静态变量 | 简单、无销毁需求 | 代码最简洁,编译器自动线程安全 | 不能显式销毁 |
std::call_once + unique_ptr |
需要显式销毁 | 线程安全,资源可被清理 | 需要额外头文件 |
| 双重检查锁 | 兼容旧编译器 | 延迟初始化 | 实现复杂,潜在错误 |
shared_ptr |
需要共享实例 | 自动管理生命周期 | 可能出现循环引用 |
| 读写锁 | 读多写少 | 高并发读 | 复杂,写锁开销 |
在大多数现代 C++ 项目中,局部静态变量(Meyers Singleton)已成为默认首选,因为它实现最简洁、性能最优,并且在 C++11 之后已被标准保证线程安全。只有在特殊需求(如显式销毁、共享实例或读写锁优化)下才考虑其他实现。
小结
- 线程安全:C++11 引入的局部静态变量或
std::call_once可轻松实现。 - 延迟初始化:如果想避免在程序启动时就实例化,可以使用
std::call_once。 - 生命周期管理:若需要在程序结束前释放资源,考虑
std::unique_ptr或std::shared_ptr。 - 性能考虑:在多线程读多写少的场景下,读写锁可进一步提升性能,但要权衡实现复杂度。
通过以上方法,你可以根据项目需求灵活选择最合适的单例实现方案,确保代码既简洁又安全。