在多线程环境下,单例模式的实现往往会成为安全性和性能的双重挑战。下面我们从两个角度来探讨在C++17中实现线程安全单例的几种常见方案,并对比它们的优缺点。
1. Meyers’ Singleton(局部静态对象)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11以后初始化是线程安全的
return instance;
}
// 删除拷贝构造和赋值操作,防止外部复制
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
优点
- 代码简洁,几乎不需要额外的同步机制。
- 对象生命周期由编译器管理,避免手工删除。
- 适用于大多数需求,尤其是在懒加载(lazy loading)时。
缺点
- 对象在程序结束时才会析构,若需要提前释放资源,需自行手动销毁或使用
std::unique_ptr包装。 - 仅适用于C++11及以后编译器,旧编译器不支持。
2. 双重检查锁(Double-Checked Locking)
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;
}
// 同上删除拷贝构造与赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
优点
- 通过原子操作减少了锁的粒度,仅在第一次实例化时进入互斥。
缺点
- 代码相对复杂,需要 careful memory ordering,易出错。
- 仍然需要手动删除实例(如在
atexit注册销毁函数)以避免资源泄漏。
3. 显式销毁 + std::unique_ptr
class Singleton {
public:
static Singleton& instance() {
static std::once_flag flag;
static std::unique_ptr <Singleton> ptr;
std::call_once(flag, [](){
ptr.reset(new Singleton());
std::atexit(&Singleton::destroy);
});
return *ptr;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static void destroy() {
ptr.reset(); // 释放资源
}
};
优点
std::call_once保证一次性初始化,线程安全。- 使用
std::unique_ptr自动管理内存,避免泄漏。 - 通过
atexit可确保在程序结束时显式销毁。
缺点
- 需要额外的
ptr声明在类外,略显繁琐。 - 在多次
instance()调用后,析构仍在程序结束时,若需要提前释放,需额外操作。
4. 线程局部存储(TLS)方式
如果每个线程需要独立的单例实例,可以使用线程局部存储:
class ThreadSingleton {
public:
static ThreadSingleton& instance() {
thread_local ThreadSingleton instance; // 每个线程创建自己的实例
return instance;
}
ThreadSingleton(const ThreadSingleton&) = delete;
ThreadSingleton& operator=(const ThreadSingleton&) = delete;
private:
ThreadSingleton() = default;
~ThreadSingleton() = default;
};
优点
- 每个线程都拥有自己的实例,避免了跨线程共享问题。
- 简单易懂,使用
thread_local关键字即可。
缺点
- 不是传统意义上的单例(多实例),仅适用于特定需求。
- 对于需要跨线程共享资源的情况不适用。
5. 评估与选择
| 方案 | 线程安全性 | 资源释放 | 适用场景 | 代码复杂度 |
|---|---|---|---|---|
| Meyers’ | ✅ | 程序结束 | 需要懒加载、资源不需要提前释放 | 简单 |
| 双重检查锁 | ✅ | 需手动释放 | 性能敏感、需要早期释放 | 较复杂 |
std::call_once + unique_ptr |
✅ | 自动释放 | 需要显式销毁 | 中等 |
| TLS | ✅ | 每线程自行销毁 | 线程局部单例 | 简单 |
- 如果你使用的是C++11及以后且不需要提前销毁资源,推荐使用 Meyers’ Singleton,最简单、最稳健。
- 如果你在资源释放时有特殊需求(如早期关闭数据库连接),可考虑
std::call_once+unique_ptr或 双重检查锁。 - 如果每个线程需要独立实例,使用 TLS。
6. 小结
C++17 提供了丰富的原子、锁以及线程局部存储机制,使得实现线程安全单例变得既灵活又高效。最重要的是,根据业务需求选择最合适的实现方案,而不是盲目追求最“优雅”的代码。通过对比上述方案,你可以在安全性、性能与可维护性之间找到最佳平衡点。