在C++中,单例模式常用于需要全局唯一实例的场景,例如日志系统、配置管理器或数据库连接池。实现单例时的主要难点在于如何保证线程安全,同时避免不必要的性能开销。下面我们分别介绍两种常见实现:Meyers单例(C++11之后的线程安全局部静态)和双重检查锁(Double-Check Locking,DCL)结合C++11原子操作的方案。
1. Meyers单例(C++11之后的线程安全局部静态)
class Logger {
public:
static Logger& instance() {
static Logger logger; // C++11保证线程安全的初始化
return logger;
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(mutex_);
std::cout << msg << std::endl;
}
private:
Logger() = default;
~Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
std::mutex mutex_;
};
优点
- 简洁:只需一行代码即可完成实例化。
- 线程安全:自C++11起,局部静态变量的初始化是线程安全的。
- 延迟初始化:只有第一次调用
instance()时才会构造对象。
缺点
- 销毁顺序:若在多线程环境中程序终止,可能会出现“静态析构顺序问题”。可通过显式销毁函数或
std::atexit解决。
2. 双重检查锁(Double-Check Locking)与原子操作
早期的C++实现中常用双重检查锁来延迟初始化并减少锁开销。现代C++可以结合std::atomic和std::call_once进一步简化。
传统双重检查锁(不推荐)
class Config {
public:
static Config* instance() {
if (!ptr_) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex_);
if (!ptr_) { // 第二次检查
ptr_ = new Config();
}
}
return ptr_;
}
private:
Config() = default;
static Config* ptr_;
static std::mutex mutex_;
};
Config* Config::ptr_ = nullptr;
std::mutex Config::mutex_;
问题:在某些编译器/硬件上,内存重排可能导致ptr_在构造完成前被写入,导致其他线程获取到不完整的对象。
使用std::call_once(推荐)
class Config {
public:
static Config& instance() {
std::call_once(flag_, [](){ ptr_ = new Config(); });
return *ptr_;
}
private:
Config() = default;
static Config* ptr_;
static std::once_flag flag_;
};
Config* Config::ptr_ = nullptr;
std::once_flag Config::flag_;
std::call_once确保只执行一次初始化,并且对所有线程都是可见的。- 省略手动锁,代码更简洁且安全。
3. 现代C++实现:std::shared_ptr与std::make_shared
如果单例对象需要动态释放或需要共享所有权,可使用std::shared_ptr:
class Service {
public:
static std::shared_ptr <Service> instance() {
std::call_once(flag_, [](){
ptr_ = std::make_shared <Service>();
});
return ptr_;
}
private:
Service() = default;
static std::shared_ptr <Service> ptr_;
static std::once_flag flag_;
};
std::shared_ptr <Service> Service::ptr_ = nullptr;
std::once_flag Service::flag_;
- 通过
std::make_shared一次性分配对象和控制块,减少内存碎片。 std::shared_ptr在程序结束时会自动析构,避免手动管理。
4. 小结
| 方案 | 线程安全 | 代码量 | 典型使用场景 |
|---|---|---|---|
| Meyers单例 | ✅ | 1行 | 只需一次构造,无需手动销毁 |
| 双重检查锁 | ⚠️ 旧版 | 约30行 | 传统实现,易出错 |
std::call_once |
✅ | 约10行 | 推荐现代C++实现 |
std::shared_ptr + std::call_once |
✅ | 约10行 | 需要共享所有权或动态释放 |
- 对于C++11及以后,最推荐的做法是使用
std::call_once(或Meyers单例),它既安全又简洁。 - 若对单例生命周期有特殊需求(例如在多进程间共享),则需考虑更复杂的方案(如映射文件或信号量)。
提示:在高并发场景下,避免频繁锁定单例内部对象。可使用细粒度锁或无锁算法来提升性能。