在现代 C++(尤其是 C++11 及以后版本)中,实现线程安全的单例模式已经变得相对简单。以下从基本实现、常见误区以及性能考虑几个角度展开讨论。
1. Meyers 单例(局部静态变量)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 起线程安全
return instance;
}
// 其他成员函数
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
- 线程安全:从 C++11 开始,局部静态变量的初始化是按需执行并且线程安全的。无需显式锁。
- 懒加载:第一次调用
instance()时才创建对象,节省资源。 - 销毁顺序:程序退出时会自动销毁,且销毁顺序由编译器保证。
常见误区:如果你在 C++11 之前的编译器上使用此模式,需要手动加锁;否则可能出现“双重检查锁定”问题。
2. 双重检查锁定(Double-Checked Locking)
class Singleton {
public:
static Singleton* instance() {
if (!ptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mtx);
if (!ptr) { // 第二次检查
ptr = new Singleton();
}
}
return ptr;
}
private:
Singleton() = default;
static Singleton* ptr;
static std::mutex mtx;
};
Singleton* Singleton::ptr = nullptr;
std::mutex Singleton::mtx;
- 适用于 C++11 之前:若编译器不支持 C++11 的局部静态变量,双重检查锁定可以实现线程安全。
- 注意内存可见性:必须使用
std::atomic或std::mutex,否则可能因为指令重排导致可见性问题。 - 性能:锁的开销只在第一次创建实例时出现,之后无锁。
3. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag, []{ instance_ = new Singleton(); });
return *instance_;
}
private:
Singleton() = default;
static Singleton* instance_;
static std::once_flag flag;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag;
- 更安全:
std::call_once保证了单次执行且是线程安全的。 - 可读性:代码意图明确,适用于需要在函数内部执行一次性初始化的情况。
4. 对象生命周期管理
- 栈式单例:使用局部静态对象,生命周期由程序退出时自动结束。
- 堆式单例:如果需要手动控制销毁顺序(例如需要在全局静态对象之前释放资源),可以配合
std::unique_ptr使用:std::unique_ptr <Singleton> instance_;
5. 性能与可扩展性
- 局部静态变量 具有最小的锁开销,但在极端高并发场景下首次初始化仍可能成为瓶颈。
std::call_once内部使用的平台原语(如pthread_once),性能通常优于显式锁。- 懒加载 vs 预加载:若单例创建代价高且可能在多线程场景中频繁访问,建议在程序启动阶段就创建(例如在
main()开头),以避免运行时延迟。
6. 示例:线程安全的数据库连接池
class DBPool {
public:
static DBPool& get() {
static DBPool pool; // Meyers 单例
return pool;
}
Connection* getConnection() {
std::lock_guard<std::mutex> lock(pool_mtx);
// 返回一个可用连接,或创建新连接
}
private:
DBPool() { /* 初始化连接池 */ }
std::mutex pool_mtx;
std::vector<Connection*> connections;
};
- 使用场景:多线程 Web 服务器中统一获取数据库连接,保证连接池线程安全且使用效率高。
7. 小结
- 对于 C++11 及以后,Meyers 单例是最推荐的实现方式,简单、线程安全且性能优秀。
- 如果需要手动控制初始化时机或销毁顺序,
std::call_once提供了更细粒度的控制。 - 对于旧标准,双重检查锁定与显式
std::mutex仍可行,但需注意指令重排与可见性。 - 关注对象生命周期、资源释放与性能瓶颈,是实现高质量单例模式的关键。
通过上述方法,你可以在 C++ 项目中轻松实现线程安全且高效的单例模式。