在现代C++中,单例模式经常被用来控制全局资源的访问,例如日志系统、数据库连接池或全局配置管理。实现一个既安全又高效的单例在多线程环境中尤为关键。下面我们将比较两种常见实现:C++11 标准的 Meyers Singleton(局部静态变量)和传统 双重检查锁定(Double-Checked Locking, DCL)。
1. Meyers Singleton(局部静态变量)
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_;
};
优点
- 简洁:只需一行代码即可完成线程安全的单例。
- 延迟初始化:对象在第一次调用
instance()时才被创建。 - 异常安全:若构造函数抛异常,后续调用会再次尝试初始化。
缺点
- 不可定制销毁顺序:如果需要在程序退出前按特定顺序销毁单例,局部静态变量的销毁顺序不可控。
- 无法延迟销毁:除非使用
std::unique_ptr包装,单例会在程序结束时自动销毁。
2. 双重检查锁定(DCL)
DCL 通过在多线程访问时只在首次创建时加锁,随后通过检查实例是否为空来避免多余锁的开销。传统实现如下:
class Config {
public:
static Config* instance() {
if (instance_ == nullptr) { // 第一层检查
std::lock_guard<std::mutex> lock(mutex_);
if (instance_ == nullptr) { // 第二层检查
instance_ = new Config();
}
}
return instance_;
}
void set(const std::string& key, const std::string& value) {
std::lock_guard<std::mutex> lock(mapMutex_);
configMap_[key] = value;
}
std::string get(const std::string& key) const {
std::lock_guard<std::mutex> lock(mapMutex_);
auto it = configMap_.find(key);
return (it != configMap_.end()) ? it->second : "";
}
private:
Config() = default;
~Config() = default;
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;
static Config* instance_;
static std::mutex mutex_;
mutable std::mutex mapMutex_;
std::unordered_map<std::string, std::string> configMap_;
};
Config* Config::instance_ = nullptr;
std::mutex Config::mutex_;
优点
- 可定制销毁:可以在程序任意位置显式调用
delete instance_,控制销毁顺序。 - 适用于C++03:在 C++11 之前,这是常用的线程安全单例实现。
缺点
- 复杂度高:需要手动维护锁、指针、检查。
- 易出错:如果忘记使用
volatile(C++11 之前)或未遵循内存模型,可能导致“读到未初始化的实例”。 - 性能略逊:即使在已初始化后,第一次访问仍需一次
nullptr检查。
3. 何时使用哪种实现?
| 场景 | 推荐实现 | 说明 |
|---|---|---|
| 需要 C++11 或更高版本 | Meyers Singleton | 简洁、安全、无锁开销。 |
| 需要在 C++03 环境下实现 | DCL | 兼容旧编译器,需注意线程安全细节。 |
| 需要可定制销毁顺序 | DCL | 或者在 Meyers Singleton 中使用 std::unique_ptr 结合 std::atexit 手动销毁。 |
| 需要在静态初始化阶段访问 | Meyers Singleton | 对静态构造顺序敏感时不建议使用。 |
4. 进一步提升性能的技巧
- 使用
std::atomic:在 DCL 中将实例指针声明为std::atomic<Config*>,避免因指针复用导致的悬挂指针。 - 懒加载与懒销毁:结合
std::unique_ptr与std::call_once,在首次访问时创建,在程序退出前手动销毁。 - 线程本地存储(TLS):如果单例中的数据与线程无关,避免对共享资源加锁,改用
thread_local变量实现线程局部单例。
5. 小结
- C++11 及以后:首选 Meyers Singleton,代码最简洁,安全性得到语言标准保证。
- C++03 或需要自定义销毁:双重检查锁定是可行的,但要非常小心实现细节,避免并发错误。
- 总体思路:始终先考虑线程安全和性能,再做实现细节的权衡。
通过本文的对比与示例,相信你可以在实际项目中选择合适的单例实现方案,既满足线程安全需求,又保持代码可维护性。