对象性能·单例模式·享元模式

 

单例模式保证系统中只有一个实例 享元模式既保证了细粒度的对象,又减少了内存负担

  • 单例模式保证系统中只有一个实例
  • 享元模式既保证了细粒度的对象,又减少了内存负担

对象性能模式

  • 面向对象解决了抽象问题
    • 但付出了一定代价
    • 在某些情况下面对象的成本要谨慎处理
  • 典型模式
    • Singleton
    • Flyweight

单例模式

动机

  • 软件系统中,有一种特殊的类,在系统中必须保证只有一个实例
    • 又保证逻辑正确又及良好的性能
  • 如何绕过常规构造器
    • 保证一个类只有一个实例

模式定义

  • 保证一个类只有一个实例,并提供该实例的全局访问点

类图

初始设计

  • 单例类

    class Singleton{
    // 私有函数,外界不能自己调用构造函数
    private:
        Singleton();
        Singleton(const Singleton& other);
    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;
    }
    
    • 多线程时
      • 线程A执行 if (m_instance == nullptr)
      • 线程B也执行 if (m_instance == nullptr) 判断
      • 此时都是真,则线程A/B都会创建Singleton实例
  • 多线程版本

    • 加锁

      //线程安全版本,但锁的代价过高
      Singleton* Singleton::getInstance() {
          Lock lock;
          if (m_instance == nullptr) {
              m_instance = new Singleton();
          }
          return m_instance;
      }
      
      • 加锁成本太高,即使 m_instance != nullptr 也要执行加锁和释放锁
        • m_instance创建成功后,就不需要再加锁操作了
        • 在高并发场景下,锁的成本不可忽略
    • 双检查

      Singleton* Singleton::getInstance() {
          if(m_instance==nullptr){
              Lock lock;
              if (m_instance == nullptr) {
                  m_instance = new Singleton();
              }
          }
          return m_instance;
      }
      
      • 先查询m_instace是否为空
        • 不空不加锁,直接返回实例
        • 空,则进入实例化过程
          • 实例化过程还是要加锁
          • 仍是防止多次实例化
        • 锁前检查是用于快速返回实例操作
        • 锁后检查是用于安全创建实例操作
      • 双检查锁,由于编译器的reorder导致不安全
    • reorder的应对

      m_instance = new Singleton();
      
      • 正常流程
        • 分配内存
        • 调用构造器,对内存初始化
        • 内存地址赋值给m_instance
      • 编译器优化(reorder)
        • 分配内存
        • 内存地址赋值给m_instance
        • 调用构造器,对内存初始化
      • 导致的问题
        • 线程A
          • 分配内存
          • 内存地址赋值给m_instance
        • 线程B
          • 进入getInstance()
          • 执行第一步:if (m_instance == nullptr)
          • 发现非空,直接返回这个m_instance
          • 但此时线程A还未执行构造函数,导致线程B得到的是未初始化的实例
    • 使用volatile禁止对m_instance操作的编译器优化

      Singleton* Singleton::getInstance() {
          volatile m_instance;
      
          if(m_instance==nullptr){
              Lock lock;
              if (m_instance == nullptr) {
                  m_instance = new Singleton();
              }
          }
          return m_instance;
      }
      

享元模式

动机 Motiveation

  • 使用面向对象的技术,会使系统中很快充满细粒度的对象
    • 带来很高的运行时代价
    • 主要是内存方面
  • 如何避免大量细粒度对象
    • 让客户端仍能使用对象编程

模式定义

  • 使用共享技术,有效支持大量细粒度的对象

结构

初始设计

  • 字库引擎
    • 每一个字符都应该是一个字体类
    • 几万的字符,就要有几万个字体类

字体类

class Font {
private:
    //unique object key
    string key;
    //object state
    //....
public:
    Font(const string& key){
        //...
    }
};
  • 通过字符key找对应的字体

重构设计

class FontFactory{
private:
    map<string,Font* > fontPool;
    
public:
    Font* GetFont(const string& key){
        // 字体对象池
        map<string,Font*>::iterator item=fontPool.find(key);
        if(item!=footPool.end()){
            return fontPool[key];
        } else{
            Font* font = new Font(key);
            fontPool[key]= font;
            return font;
        }
    }
    
    void clear(){
        //...
    }
};

java, c++会在编译器层面实现享元模式

要点总结

  • 面向对象很好的解决了抽象问题,但付出了一定代价
    • flyweith解决的是代价问题
  • flyweight通过共享减低系统中的对象个数
    • 降低细粒度对系统的内存压力
    • 注意对象状态的处理
  • 多少对象数量合适,需要具体评估