• 设计模式 —— 单例模式


    “对象性能”模式

    面向对象很好的解决了“抽象”的问题,但是不可避免付出一定代价,如虚函数。通常情况,面向对象的成本可忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。

    典型模式

    • 单件模式
    • 享元模式

    单例模式

    动机

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

    如何实现:

     1 class Singleton{
     2 private:
     3     Singleton();
     4     Singleton(const Singleton& other);
     5 public:
     6     static Singleton* getInstance();
     7     static Singleton* m_instance;
     8 };
     9 
    10 Singleton* Singleton::m_instance=nullptr;
    11 
    12 //线程非安全版本
    13 Singleton* Singleton::getInstance() {
    14     if (m_instance == nullptr) {
    15         m_instance = new Singleton();
    16     }
    17     return m_instance;
    18 }
    

    首先显示声明构造函数与拷贝构造函数为私有的,避免外界调用。
    其中静态方法getInstance(),在第一次调用时,条件为真,创建对象,后面再调用时,判断条件不成立,就一直只有一个对象。该方法在单线程环境下是安全的。但是在多线程条件下,就不安全。例如在ThreadA执行完14行还未执行15行new,此时ThreadB分配到时间片,执行14行也会进入到执行15行,所以多线程环境下对象可能会被创建多次。

    那么如何解决?有以下思路:

    加锁

    1 //线程安全版本,但锁的代价过高
    2 Singleton* Singleton::getInstance() {
    3     Lock lock;
    4     if (m_instance == nullptr) {
    5         m_instance = new Singleton();
    6     }
    7     return m_instance;
    8 }

    虽然能保证线程安全,但是锁的代价太高。分析如下:
    假设线程A已经进到行4,此时线程B分到时间片,想要调用,到第3行就会不成立。如果对象已经创建,每次执行if判断都不会进入new,所以整个调用都是在判断与返回,即是在读变量m_instance,而对于读操作,是不需要加锁的。所有此时的锁会造成浪费(比如等待,锁自己也是种资源),尤其是在高并发的场景下。于是有了双检查锁:

     1 //双检查锁,锁前检查锁后检查
     2 Singleton* Singleton::getInstance() {
     3     if(m_instance == nullptr){
     4         Lock lock;
     5         if(m_instance == nullptr) {
     6             m_instance = new Singleton();
     7         }
     8     }
     9     return m_instance;
    10 }

    锁前检查是为了让线程判断到对象已创建时,不用访问锁,并直接返回,这样就减少开销。在锁后检查是为了当两线程都进入第一个 if(m_instance == nullptr) 后,防止多次创建对象。

    但是,双检查锁会由于内存读写reorder而失效
    通常情况,代码编译时会生成指令序列,且会认为会按照指令序列执行。代码是以指令形式来抢占CPU的时间片的。以第6行为例:m_instance = new Singleton();
     我们假设该语句会有以下几个部分:
      Step1 :先分配内存
      Step2 :调用SingleTon的构造器并对内存进行初始化
      Step3 :把指向内存的指针赋值给m_instance


    以上三个步骤是人认为的,但经编译器优化,CPU有可能会reorder,如下:
      Step1 :先分配内存
      Step2 :把指向内存的指针赋值给m_instance
      Step3 :调用SingleTon的构造器并对内存进行初始化

    那么就可能出现这种情况:

    线程A执行到6,先分配内存,在将指向内存的指针赋给m_instance,此时轮到线程B,线程B判断到m_instance不为空,就直接返回对象,但是此时该对象还没有被构造。
    所有的编译器,如果不对双检查锁的reorder漏洞处理,不能使用双检查锁,出错的概率很高。
    对于Java,C#语言可以采用volatile处理,C++只有在11后才有解决方案:

     1 std::atomic<Singleton*> Singleton::m_instance;
     2 std::mutex Singleton::m_mutex;
     3 
     4 Singleton* Singleton::getInstance() {
     5     Singleton* tmp = m_instance.load(std::memory_order_relaxed);
     6     std::atomic_thread_fence(std::memory_order_acquire);
     7     if(m_instance == nullptr){
     8         std::lock_guard<std::mutex> lock(m_mutex);
     9         tmp = m_instance.load(std::memory_order_relaxed);
    10         if(m_instance == nullptr) {
    11             m_instance = new Singleton();
    12     std::atomic_thread_fence(std::memory_order_release);
    13     m_instance.store(tmp, std::memory_order_relaxed);
    14 
    15         }
    16     }
    17     return tmp;
    18 }

    要点总结

    • 单例模式中的实例构造器可以设置为protected以允许子类派生。
    • 单例模式一般不要支持拷贝构造和clone接口,避免导致多个实例对象
    • 如何实现多线程环境下的安全的单例模式,注意双检查锁的实现。

    更多更详细的单例模式实现见:C++设计模式——单例模式

  • 相关阅读:
    【转帖】流程与IT管理部——IT支撑业务变革的必然趋势
    【转帖】39个让你受益的HTML5教程
    【转帖】2015年2月份最佳的免费 UI 工具包
    【消息】Pivotal Pivots 开源大数据处理的核心组件
    【转帖】创业者,你为什么这么着急?
    教程:SpagoBI开源商业智能之XML Template 图表模板
    教程:Spagobi开源BI系统 Console报表设计教程
    【转帖】Mysql多维数据仓库指南 第一篇 第1章
    Kiss MySQL goodbye for development and say hello to HSQLDB
    梯度消失和梯度爆炸问题详解
  • 原文地址:https://www.cnblogs.com/y4247464/p/15472044.html
Copyright © 2020-2023  润新知