在现代 C++ 开发中,单例模式经常用于共享资源管理,例如日志系统、配置中心或数据库连接池。实现一个线程安全的单例,既要保证只创建一次实例,又要避免在多线程环境下的竞态条件。下面从几种常见实现方式入手,逐步剖析其优缺点,并给出最佳实践。
1. Meyers 单例(C++11 及以后)
class Logger {
public:
static Logger& getInstance() {
static Logger instance; // 线程安全的局部静态变量
return instance;
}
private:
Logger() = default;
~Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
};
优点
- 极简,几行代码即可完成。
- 采用局部静态变量,C++11 标准保证了线程安全的初始化。
- 对象的生命周期与程序结束同步。
缺点
- 无法自定义销毁时机(例如需要在某个特定点释放资源)。
- 对于多线程启动顺序不确定的情况,可能出现 “static initialization order fiasco” 的风险,尽管 C++11 解决了大部分,但仍需注意跨文件静态对象。
2. 带锁的懒汉式单例
class ConfigManager {
public:
static ConfigManager* getInstance() {
if (!instance) {
std::lock_guard<std::mutex> lock(mutex_);
if (!instance) {
instance = new ConfigManager();
}
}
return instance;
}
private:
ConfigManager() = default;
~ConfigManager() = default;
static ConfigManager* instance;
static std::mutex mutex_;
};
ConfigManager* ConfigManager::instance = nullptr;
std::mutex ConfigManager::mutex_;
优点
- 可以手动释放
instance,适合需要在程序中间清理单例的场景。 - 可配合
std::unique_ptr自动销毁。
缺点
- 双重检查锁(double-checked locking)在 C++11 之前实现不安全,但在 C++11 之后可以安全使用
std::atomic或std::call_once。 - 代码冗长,维护成本高。
3. std::call_once + std::once_flag
class ThreadSafeSingleton {
public:
static ThreadSafeSingleton& getInstance() {
std::call_once(initFlag, [](){ instance.reset(new ThreadSafeSingleton()); });
return *instance;
}
private:
ThreadSafeSingleton() = default;
~ThreadSafeSingleton() = default;
static std::unique_ptr <ThreadSafeSingleton> instance;
static std::once_flag initFlag;
};
std::unique_ptr <ThreadSafeSingleton> ThreadSafeSingleton::instance;
std::once_flag ThreadSafeSingleton::initFlag;
优点
- 仅初始化一次,性能更好。
- 语义清晰,避免了手动锁。
缺点
- 需要手动维护
std::unique_ptr,稍显繁琐。
4. 对象池 + 原子引用计数
在高性能服务器中,单例可能会被频繁创建与销毁。可以使用对象池结合 std::shared_ptr,并用 std::atomic 控制实例计数。
class ObjectPool {
public:
static std::shared_ptr <Worker> getWorker() {
std::shared_ptr <Worker> worker = pool_.tryPop();
if (!worker) worker = std::make_shared <Worker>();
return worker;
}
private:
static ThreadSafeStack<std::shared_ptr<Worker>> pool_;
};
优点
- 高效复用资源,减少内存分配。
- 线程安全,且
shared_ptr自动管理生命周期。
缺点
- 对象池实现复杂,需要考虑缓存大小、回收策略等。
5. 现代 C++ 推荐方案
如果你仅仅需要一个单例对象,Meyers 单例 已经足够安全、简洁。若需手动销毁或想在单例中使用 std::shared_ptr 或 std::unique_ptr 管理子资源,建议采用 std::call_once + std::unique_ptr 组合。
class ResourceHub {
public:
static ResourceHub& instance() {
std::call_once(flag_, [](){ inst_ = std::make_unique <ResourceHub>(); });
return *inst_;
}
// 资源管理接口
void addResource(const std::string& key, std::shared_ptr <Resource> res) {
std::lock_guard<std::mutex> lock(mutex_);
resources_[key] = res;
}
private:
ResourceHub() = default;
~ResourceHub() = default;
std::unordered_map<std::string, std::shared_ptr<Resource>> resources_;
std::mutex mutex_;
static std::unique_ptr <ResourceHub> inst_;
static std::once_flag flag_;
};
std::unique_ptr <ResourceHub> ResourceHub::inst_;
std::once_flag ResourceHub::flag_;
6. 关键注意点
| 事项 | 说明 |
|---|---|
| 销毁顺序 | 对跨模块的静态对象,使用 std::call_once 可避免初始化顺序问题。 |
| 多线程性能 | std::call_once 的开销小于普通互斥锁;在高并发环境下尤其重要。 |
| 懒加载 vs 立即加载 | 如果实例创建成本高,建议懒加载;否则可以在程序启动时就创建。 |
| 异常安全 | 使用 RAII(如 std::unique_ptr)可避免内存泄漏。 |
7. 结语
在 C++ 开发中,线程安全的单例并非难题,只要选对合适的实现模式即可。掌握 Meyers、std::call_once、以及对象池等技术,你就能在多线程环境下安全、低成本地共享资源。希望这篇文章能帮助你在项目中快速实现高效、稳健的单例模式。