单例模式和享元模式

单例模式(Singleton)

在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性以及良好的效率

// 饿汉式:
class SingletonHungry{
private:
    SingletonHungry() {}
    static SingletonHungry *instance;
public:

    SingletonHungry(const SingletonHungry *other) = delete;
    static SingletonHungry *getInstance(){
        return instance;
    }
};

SingletonHungry *SingletonHungry::instance = new SingletonHungry();

// 懒汉式
class Singleton{
private:
    Singleton(); // 构造函数
    Singleton(const Singleton &other); //拷贝构造 声明为私有或=delete都可以不允许外部调用
public:
    static Singleton *getInstance();
    static Singleton *m_instance;
};
Singleton* Singleton::m_instance = nullptr;

// 实现getInstance的多个版本:

// 单线程版本
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;
}

// 双检查锁(但因为编译器可能会出于优化需要,reorder实际运行的步骤,所以会存在问题)
Singleton* Singleton::getInstance(){
    if (m_instance == nullptr){
        Lock lock;
        if (m_instance == nullptr){
            // 我们预想这条指令的三个步骤 1.分配一段内存 2.执行构造,初始化 3.返回内存地址
            m_instance = new Singleton(); 
        }// 但实际上编译器可能会按1、3、2执行,如果在执行到3还没执行2的过程中切了别的线程执行getInstance
        //那么会导致m_instance不为空指针,但指向的内存无效,导致后续代码报错
    }
    return m_instance;
}


// C++ 11后的跨平台实现
std::atomic<Singleton *> Singleton::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<std::mutex> 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
}

// 新增
// 或者用volatile实现会更简单(但好像不能跨平台?)
// 懒汉式(double check + volatile版):
class SingletonLazyDoubeCheck{
private:
    SingletonLazyDoubeCheck() {}
    static SingletonLazyDoubeCheck *instance;
    static std::mutex mtx;
public:
    SingletonLazyDoubeCheck(const SingletonLazyDoubeCheck &other) = delete;

    static SingletonLazyDoubeCheck *getInstance(){
        if (instance == nullptr){
            std::unique_lock<std::mutex> lock(mtx);
            if (instance == nullptr){
                volatile auto temp = new SingletonLazyDoubeCheck();
                instance = temp;
            }
        }
        return instance;
    }
};
// 直接用Static 定义getInstance函数,并在里面用static定义所需实例变量,返回,也能保证线程安全(C++11后)
class SingletonLazy{
private:
    SingletonLazy() {}
public:
    SingletonLazy(const SingletonLazy &other) = delete;
    static SingletonLazy *getInstance(){
        static SingletonLazy inst;
        return &inst;
    }
};

享元模式(Flyweight)

在软件系统采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价————主要指内存需求方面的代价
享元模式主要是在避免大量细粒度对象问题的同时,让外部客户程序仍然能够透明地使用面向对象的方式进行操作。
即使用共享技术

一个例子:

// 一个font类描述字体
class Font{
private:
    string fontName;
    // ...
public:
    Font(const string &name) {}
};

class FontFactory{
private:
    map<string, Font*> fontPool;    // 建立字体名到字体指针的映射,作为对象池
public:
    Font *GetFont(const string &name){
        map<string, Font*>::iterator it = fontPool.find(name);
        if (it != fontPool.end()){      // 如果共享池里有,则直接返回
            return fontPool[name];
        }
        Font *f = new Font(name);       // 否则才创建新对象,并且新对象入池
        fontPool[name] = f;
        return f;
    }
};

  • 整个设计模式里,只有Singleton和Flyweight是解决性能问题的,其他都是解决抽象性问题,提高复用性。
  • 通常对象池里的对象创建出来后都是只读的,避免多处使用同一实际对象并存在修改的问题。
  • 对象数量太多才会导致对象内存开销较大,具体要对对象的大小和创建的数量进行评估,就判断是否有用享元模式的必要。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注