在多线程环境下,单例模式常常会出现竞态条件。为了保证单例对象在任何线程中只被初始化一次,C++提供了多种安全实现手段。下面从几种常见的方法展开讨论,并给出完整示例代码。
1. 局部静态变量(C++11 之后)
C++11 起,函数内部的局部静态变量初始化是线程安全的。只要把单例对象放在一个函数内部的 static 变量即可。
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 线程安全初始化
return instance;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
优点:
- 代码简洁,易于维护。
- 避免了手动管理
std::call_once或互斥锁的细节。
缺点:
- 需要 C++11 及以后标准。
2. std::call_once + std::once_flag
如果你想兼容 C++98/03 或对初始化过程有更细粒度控制,可以使用 std::call_once 与 std::once_flag。
class Singleton {
public:
static Singleton& getInstance() {
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_;
优点:
- 兼容老标准(需要 ` ` 头文件)。
- 可以在初始化时执行更复杂的逻辑。
缺点:
- 需要手动销毁单例(如果不销毁,则在程序结束时自动释放)。
3. 双重检查锁(Double-Check Locking,DCL)
传统的 DCL 方案在 C++ 之前经常被使用,但在早期的 C++ 编译器下存在内存模型不安全的问题。只要使用 std::atomic 或在 C++11 之后的标准下正确使用 std::mutex,就能安全实现。
class Singleton {
public:
static Singleton* getInstance() {
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_;
优点:
- 只在第一次初始化时获取锁,之后访问速度极快。
缺点:
- 代码相对复杂,容易出现错误。
- 需要确保内存模型正确。
4. Meyers 单例 + 析构
如果你不需要在程序结束前手动销毁单例,可以直接使用局部静态变量。若想在程序结束时按特定顺序销毁多处单例,则可以把单例包装为一个类的静态成员,并在该类析构函数中手动删除。
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
// ... 业务方法 ...
private:
Singleton() = default;
~Singleton() = default;
};
C++ 的函数内静态变量在程序退出时按创建顺序销毁,满足大多数场景。
5. 线程局部存储(TLS)实现
在某些情况下,需要每个线程都有自己的单例实例。此时可以使用 thread_local 关键字。
class ThreadSingleton {
public:
static ThreadSingleton& getInstance() {
thread_local ThreadSingleton instance;
return instance;
}
// ... 线程内部使用 ...
private:
ThreadSingleton() = default;
~ThreadSingleton() = default;
};
该方案适用于需要线程隔离的数据。
6. 现代化示例:使用 std::unique_ptr 与 std::call_once
将单例包装成智能指针,并使用 std::call_once,可以进一步避免手动 new/delete,提升异常安全。
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(flag_, [](){
instance_.reset(new Singleton());
});
return *instance_;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::unique_ptr <Singleton> instance_;
static std::once_flag flag_;
};
std::unique_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::flag_;
此实现兼具:
- 线程安全。
- 自动释放资源。
- 简洁易读。
小结
- C++11 及以后:首选局部静态变量(Meyers 单例),其初始化已保证线程安全。
- 需要兼容旧标准:
std::call_once+std::once_flag是最安全、最兼容的方案。 - 特殊需求:线程局部存储、双重检查锁、或者自定义销毁顺序。
在实际项目中,建议从最简单的方式开始,除非有特殊性能或资源管理需求,否则不必过度优化。这样既能保证代码的可维护性,又能确保在多线程环境下单例的安全性。