vlambda博客
学习文章列表

C++ 单例模式学习(Singleton)

最近在学习设计模式,学到单例模式,觉得这一讲讲的挺好的,整理一下,一起学习学习。单例模式可能大家都已经非常熟悉了。


单例模式属于 对象性 能”模
“对象性能”模式
面向对象很好地解决了“抽象”的问题,但是不可避免地要付出一定的代价。对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。
典型模式
Singleton
Flyweight(享元模式)

Singleton 单例模式
动机

l 在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率
l 如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?
l 这应该是类设计者的责任,而不是使用者的责任

通过代码示例来看看:

class Singleton{private: // 防止外部构造。 Singleton() = default;
// 防止拷贝和赋值。 Singleton& operator=(const Singleton&) = delete;  Singleton(const Singleton& singleton2) = delete;public: static Singleton* getInstance(); static Singleton* m_instance;};
Singleton* Singleton::m_instance=nullptr;//线程非安全版本Singleton* Singleton::getInstance() { if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance;}


很明显上面这种方法存在线程安全稳定,那么通常会想到用锁,比如:
//线程安全版本,但锁的代价过高Singleton* Singleton::getInstance() { Lock lock; if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance;}

但是用锁的代价太高了,因此又出现了 Double-Checked Locking Pattern (DCLP),双检查锁
//双检查锁,但由于内存读写reorder不安全Singleton* Singleton::getInstance() {  if(m_instance==nullptr){ Lock lock; if (m_instance == nullptr) { m_instance = new Singleton(); } } return m_instance;}
Double-Checked Locking Pattern (DCLP),使用两次判断来解决线程安全问题并且提高效率。 但是,在很久之后,发现Double-Checked Locking Pattern (DCLP)实际上也是存在严重的线程安全问题。
Scott Meyers and 和Alexandrescu写的一篇文章里面专门分析了这种解决方案的问题C++ and the Perils of Double-Checked Locking
文章链接如下:
https://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf

双检查锁,但由于内存读写reorder不安全
reorder不安全指的是什么呢,我们先看下面代码:
instance_ = new Singleton;
这条语句实际上做了三件事,第一:申请一块内存,第二:调用构造函数,第三:将该对象内存地址赋给instance_。

 if (instance_ == nullptr) { \\ 语句1 std::lock_guard lock(mutex_); if (instance_ == nullptr) { instance_ = new Singleton; \\ 语句2 } }

但由于编译器可能先将该内存地址赋给instance_,然后再调用构造函数。那么,如果线程A恰好申请完内存,并且将内存地址赋给instance_,但是还没调用构造函数的时候。线程B执行到语句1,判断instance_此时不为空,则返回该变量,然后调用该对象的函数,但是线程A使用的对象还没有进行构造。


不过,现在C++11 提供了解决加锁导致效率问题,如下:

//C++ 11版本之后的跨平台实现 (volatile)std::atomicSingleton::m_instance;std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence if (tmp == nullptr) { std::lock_guard lock(m_mutex); tmp = m_instance.load(std::memory_order_relaxed); if (tmp == nullptr) { tmp = new Singleton; std::atomic_thread_fence(std::memory_order_release);//释放内存fence m_instance.store(tmp, std::memory_order_relaxed); } } return tmp;}

在C++11中还提供一种方法,使得函数可以线程安全的只调用一次。即使用 std::call_once std::once_flag std::call_once 是一种lazy load的很简单易用的机制。
使用std::call_once实现单例
实现代码如下:
#include <iostream>#include <memory>#include <mutex>
class Singleton {public: static Singleton& GetInstance() { static std::once_flag s_flag; std::call_once(s_flag, [&]() { instance_.reset(new Singleton);    });
return *instance_;  }  ~Singleton() = default;
private: Singleton() = default;
Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;
private: static std::unique_ptr<Singleton> instance_;};
std::unique_ptr<Singleton> Singleton::instance_;
int main() {  Singleton& s1 = Singleton::GetInstance();  Singleton& s2 = Singleton::GetInstance(); return 0;}


单例模式的定义
保证一个类仅有一个实例,并提供一个该实例的全局访问点

结构
要点总结

l Singleton 模式中的实例构造器可以设置为protected 以允许子类派生。
l Singleton 模式一般不要支持拷贝构造方法和 clone 接口,因为这有可能导致多个对象实例,违背 Singleton 模式的初衷。
l 如何实现多线程环境下安全的 Singleton ,注意对双检查锁的正确实现。