在多线程环境下实现线程安全的单例模式是一项常见且重要的任务。C++11引入了原子操作和内存序列化,使得实现线程安全的单例变得更为简洁和高效。下面将从理论到实践,介绍几种主流实现方式,并给出示例代码。
1. 静态局部变量(Meyers单例)
C++11规定,局部静态变量的初始化是线程安全的。最简洁的实现方式就是使用静态局部变量:
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11保证线程安全
return instance;
}
// 复制构造与赋值运算符禁用
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 示例接口
void doSomething() { /* ... */ }
private:
Singleton() = default;
~Singleton() = default;
};
优点:
- 简单、易读
- 编译器自动处理线程同步
缺点:
- 如果在程序初始化时需要对单例进行延迟初始化,或者需要在单例销毁前执行特定操作,可能会遇到“static initialization order fiasco”。
2. 带双重检查锁(Double-Check Locking)
在某些旧编译器或需要显式控制初始化时,可使用双重检查锁实现线程安全单例:
class Singleton {
public:
static Singleton* instance() {
Singleton* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton();
instance_.store(tmp, std::memory_order_release);
}
}
return tmp;
}
// 禁用拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
优点:
- 延迟初始化
- 对早期 C++ 标准兼容
缺点:
- 代码较繁琐
- 需要手动处理原子与锁,容易出现错误
3. 利用 std::call_once
std::call_once 通过内部锁确保一次性初始化,既简单又可靠:
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, [](){
instance_ = new Singleton();
});
return *instance_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
优点:
- 语义清晰
- 兼容所有 C++11 及以上编译器
缺点:
- 需要手动管理指针,容易忘记释放
4. 线程安全的懒加载容器
如果你想在多线程环境下懒加载资源,同时保证单例唯一,可以将单例包装在一个 std::shared_ptr 或 std::unique_ptr 中,配合 std::call_once:
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::call_once(initFlag_, [](){
instance_ = std::make_shared <Singleton>();
});
return instance_;
}
// ...
private:
Singleton() = default;
~Singleton() = default;
static std::shared_ptr <Singleton> instance_;
static std::once_flag initFlag_;
};
std::shared_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
5. 需要注意的细节
-
析构顺序
如果单例在程序退出前需要执行清理工作,最好使用std::shared_ptr或在instance()返回前注册std::atexit清理函数,避免静态对象的销毁顺序导致访问已释放资源。 -
多次实例化
在使用dll或插件机制时,若每个模块都有自己的全局静态变量,可能会出现多份单例。解决方案是将单例实现为线程本地存储(TLS)或使用进程级别的同步机制。 -
性能考量
std::call_once只在第一次调用时加锁,其余调用几乎无开销。相比双重检查锁,它更简单且同样高效。
6. 小结
- 最推荐:使用静态局部变量(Meyers单例)或
std::call_once,两者都符合 C++11 标准,线程安全且代码简洁。 - 特殊需求:若需要显式控制初始化顺序或在特定时刻销毁,考虑
std::call_once+std::unique_ptr或std::shared_ptr。 - 旧编译器:若只能使用 C++03,双重检查锁仍然可行,但要注意编译器的内存模型支持。
通过上述方案,你可以在 C++ 项目中轻松实现线程安全的单例模式,避免多线程竞争导致的未知错误。