在多线程环境中,单例模式常用于共享资源,例如日志系统或数据库连接池。实现线程安全的单例有几种常见做法,下面详细介绍两种最常用且简洁的实现方式,并比较它们的优缺点。
1. C++11 std::call_once + std::once_flag
C++11 引入了 std::call_once 与 std::once_flag,可保证某个函数仅被调用一次,即使在多线程竞争时也不需要手动加锁。
#include <mutex>
#include <memory>
class Logger {
public:
static Logger& instance() {
std::call_once(initFlag, [](){ instancePtr.reset(new Logger); });
return *instancePtr;
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(mtx);
// 简单示例:直接输出
std::cout << msg << std::endl;
}
private:
Logger() = default;
~Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
static std::once_flag initFlag;
static std::unique_ptr <Logger> instancePtr;
std::mutex mtx;
};
std::once_flag Logger::initFlag;
std::unique_ptr <Logger> Logger::instancePtr = nullptr;
优点
- 简洁:不需要显式锁,减少代码量。
- 性能:
std::call_once内部实现为原子操作,开销低。 - 线程安全:在任何线程中调用
instance()都是安全的。
缺点
- 无法自定义销毁顺序:对象会在程序退出时被自动销毁,若有依赖关系需手动管理。
2. 局部静态变量(Meyer’s Singleton)
C++11 起,局部静态变量的初始化是线程安全的。只需将实例定义为局部静态即可。
class Config {
public:
static Config& instance() {
static Config instance; // C++11 保证线程安全
return instance;
}
// 读取配置
std::string get(const std::string& key) const {
std::lock_guard<std::mutex> lock(mtx);
auto it = data.find(key);
return it != data.end() ? it->second : "";
}
void set(const std::string& key, const std::string& value) {
std::lock_guard<std::mutex> lock(mtx);
data[key] = value;
}
private:
Config() = default;
~Config() = default;
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;
std::unordered_map<std::string, std::string> data;
mutable std::mutex mtx;
};
优点
- 代码最简:不需要额外的
once_flag或手动锁。 - 天然延迟初始化:首次调用时才会构造,避免不必要的开销。
缺点
- 无法显式销毁:如果对象的析构顺序重要,需要特殊处理(例如使用
std::shared_ptr或std::unique_ptr与自定义销毁器)。 - 不易单元测试:全局状态难以重置。
3. 比较与实践建议
| 方案 | 线程安全性 | 成本 | 可维护性 | 适用场景 |
|---|---|---|---|---|
call_once + once_flag |
✅ | 低 | ✅ | 需要显式控制初始化与销毁 |
| 局部静态(Meyer’s) | ✅ | 极低 | ✅ | 只需一次构造,销毁无关紧要 |
- 多线程竞争激烈:优先使用
std::call_once,可以在需要时再做销毁控制。 - 简单工具类:局部静态即可,代码更简洁。
4. 小结
实现线程安全单例最推荐的方式是利用 C++11 标准库的 std::call_once 与 std::once_flag,它既保证了单次初始化,又避免了显式加锁的复杂性。若项目对销毁顺序无特别需求,局部静态变量(Meyer’s Singleton)也是一种极简且高效的选择。无论采用哪种方式,都需要注意对内部成员的访问同步,避免在单例方法之外出现竞争。