在多线程环境下,单例模式是常见的设计模式之一,旨在保证某个类只有一个实例并提供全局访问点。实现线程安全的单例有多种方法,下面将分别介绍几种常见的实现方式,并讨论它们的优缺点。
1. 经典的双重检查锁(Double-Check Locking)
class Singleton {
public:
static Singleton& getInstance() {
if (!instance_) { // 第一检查
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) { // 第二检查
instance_ = new Singleton();
}
}
return *instance_;
}
// 其他成员函数
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
优点
- 只在第一次访问时加锁,后续访问性能接近无锁。
缺点
- 在C++11之前的编译器,
instance_的写入可能被编译器重排,导致读线程看到部分初始化的对象。 - 需要手动管理内存,可能会导致程序退出时资源未释放。
2. 静态局部变量(Meyers’ Singleton)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 第一次进入时初始化
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点
- 线程安全保证由 C++11 标准保证(函数内部的静态局部对象初始化是线程安全的)。
- 简单易读,无需显式锁。
- 对象在程序结束时自动析构,避免内存泄漏。
缺点
- 如果单例在程序启动前被使用,可能会产生死锁(极少见)。
- 对于需要延迟初始化时的某些特殊场景,可能不够灵活。
3. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag_, [](){
instance_ = new Singleton();
});
return *instance_;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
优点
- 与
call_once配合使用,保证只初始化一次,且线程安全。 - 与双重检查锁相比,避免了锁的手动管理。
缺点
- 需要手动释放
instance_,或者在程序结束前进行销毁。 - 代码略显冗长。
4. 通过 std::shared_ptr 或 std::unique_ptr
class Singleton {
public:
static Singleton& getInstance() {
static std::shared_ptr <Singleton> instance(new Singleton());
return *instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点
- 利用智能指针自动管理资源。
- 适合需要对单例对象进行共享引用计数的场景。
缺点
- 引入引用计数,稍微增加开销。
- 仍然依赖静态局部变量的初始化机制。
5. C++20 的 std::atomic<std::shared_ptr<>> 与 std::make_shared
如果你在 C++20 环境下,需要在多个线程间安全地共享单例对象,可以考虑:
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
auto temp = instance_.load(std::memory_order_acquire);
if (!temp) {
std::lock_guard<std::mutex> lock(mutex_);
temp = instance_.load(std::memory_order_relaxed);
if (!temp) {
temp = std::make_shared <Singleton>();
instance_.store(temp, std::memory_order_release);
}
}
return temp;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::atomic<std::shared_ptr<Singleton>> instance_;
static std::mutex mutex_;
};
std::atomic<std::shared_ptr<Singleton>> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
优点
- 线程安全且可通过
std::shared_ptr方便管理。 - 适用于需要在多线程中传递单例引用的场景。
缺点
- 代码复杂度更高。
- 依赖 C++20 特性,可能不兼容旧编译器。
小结
- 最推荐:如果你使用的是 C++11 或更高版本,最简单、最安全的做法是 Meyers’ Singleton(静态局部变量)。
- 需要显式销毁:如果你需要手动控制单例销毁时机,或在特殊多线程场景下使用,考虑
std::call_once或双重检查锁。 - 共享计数:当你希望在多个线程或模块共享同一个单例实例时,使用
std::shared_ptr或std::atomic<std::shared_ptr>更合适。
记住,单例模式的核心是“全局唯一”,但过度使用单例可能导致代码耦合度高、难以测试和维护。建议在需求明确、不可避免的场景下才使用单例,或者考虑更现代的设计方案(如依赖注入、服务定位器等)。