本篇介绍了单例模式及相关的面向对象设计原则。单例模式保证一个类仅有一个实例,并提供一个该实例的全局访问点。重点在于如何实现线程安全的单例模式。
1 经典单例模式
许多时候,我们程序中的某些对象只需要一个或者只能存在一个,比如线程池、缓存、硬件的驱动等,如果存在多个实例就会造成逻辑错误或程序异常。因此作为类的设计者,就需要保证这个类在程序中只能存在一个实例对象。
显然,利用全局静态变量似乎就可以做到,但是全局静态变量并不完美。比如我们需要唯一的一个全局静态对象,那么这个全局对象在程序一开始就会被分配内存,如果后续并没有使用到这个全局对象,就造成了资源的浪费。而使用单例模式,可以随时在需要时才创建这个唯一的对象,并保证不会出现第二个对象。
经典的单例模式实现非常简单,在类中设定一个该类的静态对象成员,将构造函数设置为 private,然后提供一个静态的 getInstance()
方法来获取全局唯一的对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Singleton { private: static Singleton* uniqueInstance; Singleton() = default; ~Singleton() = default; Singleton(const Singleton&) = delete; Singleton(Singleton&&) = delete; Singleton &operator=(const Singleton&) = delete; Singleton &operator=(Singleton&&) = delete; public: static Singleton* getInstance() { if(uniqueInstance == nullptr) { uniqueInstance = new Singleton(); } return uniqueInstance; } static void deleteInstance() { if(uniqueInstance) { delete uniqueInstance; uniqueInstance = nullptr; } } };
static Singleton* Singleton::uniqueInstance = nullptr;
|
2 线程安全的单例模式
上面的经典实现在多线程的情况下会出现问题,有可能创建出两个实例对象,例如:
因此上述实现不是线程安全的。要实现线程安全的单例模式有多种方法。
2.1 DCLP
一种经典的方法是 Double-Checked Locking Pattern (DCLP),在构造对象时使用互斥锁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class Singleton { private: static Singleton* uniqueInstance; static std::mutex m_mutex; Singleton() = default; ~Singleton() = default; Singleton(const Singleton &) = delete; Singleton(Singleton &&) = delete; Singleton &operator=(const Singleton &) = delete; Singleton &operator=(Singleton &&) = delete; public: static Singleton* getInstance() { if(uniqueInstance == nullptr) { std::lock_guard<std::mutex> lock(m_mutex); if(uniqueInstance == nullptr) { uniqueInstance = new Singleton(); } } return uniqueInstance; } static void deleteInstance() { if(uniqueInstance) { delete uniqueInstance; uniqueInstance = nullptr; } } };
static Singleton* Singleton::uniqueInstance = nullptr; static std::mutex Singleton::m_mutex;
|
但 DCLP 也不是绝对安全的,问题出在uniqueInstance = new Singleton()
这一句上,我们知道 C++ 中 new
一个对象实际上进行了三次操作:
- 首先调用
operator new
申请一块内存
- 然后调用构造函数在该内存上构造对象
- 最后将该内存地址赋值给指针
但由于编译器优化的原因,C++ 编译器可能会在构造函数不抛出异常的前提下调整语句的执行顺序,也就是上面的三步可能变成:
- 首先调用
operator new
申请一块内存
- 然后将该内存地址赋值给指针
- 最后调用构造函数在该内存上构造对象
在单线程的情况下,这样的顺序变化不会影响程序的结果,但在多线程情况下就不一定了,比如:
这种情况下,线程 A 恰好完成内存申请,并且将内存地址赋给指针,但是还没有调用构造函数,此时线程 B 执行到指针判断,判断指针不为空,于是返回该对象指针,然后调用该对象的函数,但是这时该对象还没有进行构造,就产生了错误。
DCLP 产生该问题的关键在于 new
的操作不是原子的,虽然使用互斥锁对指针的写操作uniqueInstance = new Singleton()
加了锁,但没有对之前的读操作if(uniqueInstance == nullptr)
加锁,从而产生了线程不安全问题。
因此一种解决方案是利用原子变量std::atomic
将对象指针变为原子类型,并使用std::memory_order_acquire
和std::memory_order_release
强制对该指针的内存操作顺序不变,从而避免线程不安全:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class Singleton { private: static std::atomic<Singleton*> uniqueInstance; static std::mutex m_mutex; Singleton() = default; ~Singleton() = default; Singleton(const Singleton &) = delete; Singleton(Singleton &&) = delete; Singleton &operator=(const Singleton &) = delete; Singleton &operator=(Singleton &&) = delete; public: static Singleton* getInstance() { Singleton* ins = uniqueInstance.load(std::memory_order_acquire); if(ins == nullptr) { std::lock_guard<std::mutex> lock(m_mutex); ins = uniqueInstance.load(std::memory_order_acquire); if(ins == nullptr) { ins = new Singleton(); uniqueInstance.store(ins, std::memory_order_release); } } return uniqueInstance; } static void deleteInstance() { if(uniqueInstance) { delete uniqueInstance; uniqueInstance = nullptr; } } };
static std::atomic<Singleton*> Singleton::uniqueInstance = nullptr; static std::mutex Singleton::m_mutex;
|
2.2 call_once
除了 DCLP 之外,还有一种更简单的实现线程安全的单例模式的方法,利用 C++ 的 std::call_once()
来实现,std::call_once()
可以保证函数只被线程安全的调用一次:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class Singleton { private: static Singleton* uniqueInstance; static std::once_flag m_flag; Singleton() = default; ~Singleton() = default; Singleton(const Singleton &) = delete; Singleton(Singleton &&) = delete; Singleton &operator=(const Singleton &) = delete; Singleton &operator=(Singleton &&) = delete; static void createInstance() { uniqueInstance = new Singleton(); } public: static Singleton* getInstance() { std::call_once(m_flag, createInstance); return uniqueInstance; } static void deleteInstance() { if(uniqueInstance) { delete uniqueInstance; uniqueInstance = nullptr; } } };
static Singleton* Singleton::uniqueInstance = nullptr; static std::once_flag Singleton::m_flag;
|
2.3 Meyers 单例模型
最后还有一种最为简单的实现,利用 C++ 的局部静态变量机制,C++ 的局部静态变量在第一次被调用时进行初始化,且保证只初始化一次,因此可以直接在 getInstance()
方法中初始化一个局部静态对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Singleton { private: Singleton() = default; ~Singleton() = default; Singleton(const Singleton &) = delete; Singleton(Singleton &&) = delete; Singleton &operator=(const Singleton &) = delete; Singleton &operator=(Singleton &&) = delete; public: static Singleton* getInstance() { static Singleton* uniqueInstance; return uniqueInstance; } static void deleteInstance() { if(uniqueInstance) { delete uniqueInstance; uniqueInstance = nullptr; } } };
|