单例模式(Singleton)是一种常用的软件设计模式,确保一个类只有一个实例,并提供全局访问点。随着多线程编程的普及,单例实现必须保证线程安全。下面以 C++17 为例,介绍几种典型实现方式,并分析它们的优缺点。
1. 使用局部静态变量(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() = default;
~Singleton() = default;
};
优点
- 代码简洁,几乎不需要额外的同步机制。
- C++11 之后,局部静态对象的初始化是线程安全的,编译器会自动生成必要的锁。
- 资源在第一次使用时才创建,延迟初始化。
缺点
- 只适用于 C++11 及以后版本。
- 由于使用
static对象,无法在程序结束时有序销毁(如果需要销毁顺序或自定义释放,则需要手动控制)。
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++98/03/11。
- 延迟初始化,且线程安全。
缺点
- 代码较为繁琐,容易出错。
- 在某些极端情况下,可能因为编译器优化导致多次实例化(需要使用
std::atomic及正确的内存序保证)。
3. 使用 std::call_once
class Singleton {
public:
static Singleton& instance() {
std::call_once(init_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 init_flag_;
};
std::unique_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::init_flag_;
优点
- 代码简洁,安全且跨编译器兼容。
std::call_once保证初始化只执行一次,无需手动写锁。
缺点
- 依赖
std::once_flag,C++11 之后可用。 - 与第一种实现相同,实例在首次访问时创建,无法在程序结束时自定义销毁顺序。
4. 传统懒汉式(非线程安全)+ 后期加锁
class Singleton {
public:
static Singleton& instance() {
if (!instance_) {
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) instance_ = new Singleton();
}
return *instance_;
}
private:
static Singleton* instance_;
static std::mutex mutex_;
};
该实现与双重检查锁类似,但缺乏 std::atomic 的使用,容易出现线程安全问题,推荐使用 std::call_once 或局部静态对象。
5. 采用 std::shared_ptr 与 std::weak_ptr
如果你需要在多线程中共享单例实例并允许其在无引用时自动销毁,可结合 std::shared_ptr 与 std::weak_ptr:
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
std::shared_ptr <Singleton> tmp = instance_.lock();
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance_.lock();
if (!tmp) {
tmp = std::shared_ptr <Singleton>(new Singleton);
instance_ = tmp;
}
}
return tmp;
}
private:
Singleton() = default;
static std::weak_ptr <Singleton> instance_;
static std::mutex mutex_;
};
优点
- 单例可以在不被任何线程引用时自动销毁,避免内存泄漏。
- 支持在多线程场景下安全获取实例。
缺点
- 每次调用都要进行
std::weak_ptr::lock(),会产生一定开销。 - 需要小心循环引用,避免泄漏。
小结
- C++11 以上:推荐使用 局部静态变量 或
std::call_once,两者都简单安全。 - 兼容旧标准:可使用 双重检查锁 或
std::call_once(如果支持 C++11)来实现线程安全。 - 需要自动销毁:可以使用
std::shared_ptr+std::weak_ptr的方案。
选择哪种实现,取决于项目的编译器支持、对实例生命周期的需求以及对代码可读性的要求。掌握这些实现方式后,你可以在多线程 C++ 项目中轻松、安全地使用单例模式。