在多线程环境下,单例模式的实现需要考虑并发访问导致的竞争条件。下面通过几种常见的实现方式来演示如何在C++中实现线程安全的单例模式,并分析各自的优缺点。
1. 经典双重检查锁定(Double-Check Locking)
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
if (!ptr_) { // 第一检查(非锁)
std::lock_guard<std::mutex> lock(mutex_);
if (!ptr_) { // 第二检查(加锁后)
ptr_ = new Singleton();
}
}
return *ptr_;
}
// 复制构造与赋值运算符禁止
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
~Singleton() { delete ptr_; }
static Singleton* ptr_;
static std::mutex mutex_;
};
Singleton* Singleton::ptr_ = nullptr;
std::mutex Singleton::mutex_;
优点
- 只在第一次初始化时加锁,性能相对较好。
缺点
- 需要对指针进行原子操作或使用
std::atomic<Singleton*>,否则在某些编译器或架构下可能出现指令重排序导致的可见性问题。 - 代码略显复杂。
2. C++11 的局部静态变量(Meyer’s Singleton)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 规定线程安全的局部静态初始化
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
~Singleton() {}
};
优点
- 代码简洁,完全符合 C++11 标准,编译器保证线程安全。
- 自动在程序结束时销毁。
缺点
- 对于需要在程序早期初始化(例如在
main之前)的情况,可能会出现“使用前未初始化”问题,尽管可以通过提前调用instance()解决。 - 需要 C++11 或更高版本。
3. 显式锁和一次性初始化(std::call_once)
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, []() {
ptr_ = new Singleton();
});
return *ptr_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
~Singleton() { delete ptr_; }
static Singleton* ptr_;
static std::once_flag initFlag_;
};
Singleton* Singleton::ptr_ = nullptr;
std::once_flag Singleton::initFlag_;
优点
std::call_once保证初始化只执行一次,线程安全且性能良好。- 与
Meyer's Singleton相比,可在任何线程中安全调用。
缺点
- 需要手动管理指针,容易出现内存泄漏。
- 对于全局对象销毁顺序不确定,可能导致访问已被销毁的单例。
4. 线程安全的懒加载与销毁(带计数器)
#include <atomic>
class Singleton {
public:
static Singleton& instance() {
Singleton* tmp = ptr_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = ptr_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton();
ptr_.store(tmp, std::memory_order_release);
}
}
++refCount_;
return *tmp;
}
static void release() {
if (--refCount_ == 0) {
std::lock_guard<std::mutex> lock(mutex_);
if (refCount_ == 0) {
delete ptr_;
ptr_ = nullptr;
}
}
}
private:
Singleton() {}
~Singleton() {}
static std::atomic<Singleton*> ptr_;
static std::atomic <int> refCount_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::ptr_{nullptr};
std::atomic <int> Singleton::refCount_{0};
std::mutex Singleton::mutex_;
优点
- 支持懒加载、可在多次
instance()调用后手动销毁,避免全局对象销毁顺序问题。
缺点
- 代码较为复杂,需要手动调用
release()。 - 计数错误会导致内存泄漏或提前销毁。
5. 现代 C++ 的 std::shared_ptr
#include <memory>
#include <mutex>
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::call_once(initFlag_, []() {
ptr_ = std::shared_ptr <Singleton>(new Singleton());
});
return ptr_;
}
private:
Singleton() {}
~Singleton() {}
static std::shared_ptr <Singleton> ptr_;
static std::once_flag initFlag_;
};
std::shared_ptr <Singleton> Singleton::ptr_;
std::once_flag Singleton::initFlag_;
优点
- 使用智能指针自动管理生命周期。
call_once保证线程安全。
缺点
- 需要在所有使用者中保持
std::shared_ptr的引用,若未保持会导致单例被销毁,后续访问会崩溃。
小结
- Meyer’s Singleton:最简洁、最安全,推荐在 C++11 及以上使用。
std::call_once+std::unique_ptr:在需要手动销毁或需要更细粒度控制时使用。- 双重检查锁定:兼容旧编译器,但更容易出错,建议避免。
- 计数器实现:适合需要在运行时动态销毁单例的高级场景。
在实际项目中,建议先评估单例的生命周期、使用场景以及对线程安全的严格程度,选择最符合需求的实现方式。祝编码愉快!