在现代 C++(尤其是 C++11 之后)实现线程安全的懒汉式单例变得异常简洁。最常用的做法是利用局部静态变量的线程安全初始化特性。下面给出完整示例,并讨论常见陷阱与进一步优化。
#include <iostream>
#include <mutex>
class Singleton {
public:
// 通过 getInstance() 访问单例对象
static Singleton& getInstance() {
// C++11 起局部静态变量的初始化是线程安全的
static Singleton instance; // 第一次进入此函数时才会创建
return instance;
}
// 禁止拷贝构造和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 示例成员函数
void doSomething() {
std::lock_guard<std::mutex> lock(mutex_);
std::cout << "Singleton instance address: " << this << '\n';
}
private:
Singleton() { std::cout << "Singleton constructed\n"; }
~Singleton() { std::cout << "Singleton destroyed\n"; }
std::mutex mutex_; // 用于演示线程安全访问
};
为什么这样可行?
-
局部静态变量的初始化
C++11 规定,局部静态变量在第一次使用时进行初始化,并且此过程对多线程是原子且互斥的。换句话说,static Singleton instance;在多线程环境下不会出现“双重检查锁定”的问题。 -
延迟初始化
Singleton对象只在第一次调用getInstance()时创建,满足懒汉式的需求。 -
生命周期管理
instance的生命周期由程序结束时的全局析构顺序决定,避免了手动删除的风险。
常见错误与陷阱
| 错误 | 说明 | 解决方案 |
|---|---|---|
| 双重检查锁定 | 在 C++11 之前手动实现双重检查锁定可能导致数据竞争。 | 直接使用局部静态变量或 std::call_once。 |
| 单例在多进程环境中 | 进程间并不共享内存,单例无法跨进程。 | 需要进程间通信机制(如共享内存 + 信号量)。 |
| 静态成员初始化顺序 | 静态全局对象在不同翻译单元中的初始化顺序不确定。 | 采用局部静态变量或 Meyers Singleton。 |
| 对象销毁顺序问题 | 程序结束时,单例对象可能在其他全局对象之前被销毁。 | 通过 std::atexit 注册清理函数或使用 std::unique_ptr + std::weak_ptr。 |
性能考虑
虽然局部静态变量的初始化是线程安全的,但第一次调用仍需保证互斥。若单例创建成本非常低且只在程序启动后不久被访问,这几乎不会成为瓶颈。若单例创建成本高,可以考虑:
#include <atomic>
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
Singleton* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(init_mutex_);
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton;
instance_.store(tmp, std::memory_order_release);
}
}
return *tmp;
}
// 其余同上
private:
Singleton() { /* heavy init */ }
static std::atomic<Singleton*> instance_;
static std::mutex init_mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::init_mutex_;
此实现使用原子操作 + 双重检查,减少首次访问的锁粒度,但实现更复杂。对于大多数 C++11 代码库,推荐使用局部静态变量的方式,它既安全又易于维护。
小结
- C++11 之后,局部静态变量的线程安全初始化使实现懒汉式单例变得极其简洁。
- 只需提供
static Singleton& getInstance()并删除拷贝/赋值操作即可。 - 注意线程安全、初始化顺序和销毁顺序等细节,避免常见陷阱。
- 对性能要求极高的场景,可进一步采用原子+双重检查,但代码复杂度也随之提升。
实战小贴士:在多线程程序中使用单例时,尽量把单例内部的资源访问也做成线程安全的(如使用互斥锁、原子操作等),避免出现竞争条件。