总结5.对象性能:单件模式,享元模式
声明:本栏目的 [A] 系列的学习笔记,学习对象为 B 站授课视频 C++设计模式(李建忠),参考教材为《设计模式:可复用面向对象软件的基础》。本栏目 [A] 系列文章中的图件和笔记,部份来自上述资源。
从封装变化角度对模式分类!:
- 组件协作
- 单一职责
- 对象创建
- 对象性能
面向对象很好的解决了“抽象”的问题,但是必不可免的要付出一定的代价。对于通常情况来讲,面向对象的成本都可以忽略不计。但是某些情况下,面向对象带来的成本必须谨慎处理。
典型模式
• 单件模式 Singleton
• 享元模式 Flyweight- 接口隔离
- 状态变化
- 数据结构
- 行为变化
- 领域问题
单件模式 Singleton
-
定义:保证类仅有一个实例,并提供一个该实例的全局访问点。
-
场景举例:字体模块,每一种字体在全局只需要有一个实例。
class Singleton{
private:
// 构造函数和拷贝构造函数设成私有的,外界无法使用。
// 注意不能不写,不写的话会有默认构造函数
Singleton();
Singleton(const Singleton& other);
public:
// 静态变量和静态函数
static Singleton* getInstance();
static Singleton* m_instance;
};
Singleton* Singleton::m_instance=nullptr;
//将静态变量初始成 nullptr, Singleton只能通过静态函数 getInstance进行构造。
// 如果Singleton有经过实例化,m_instance就非空,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) {
m_instance = new Singleton();
}
}
return m_instance;
}
//C++ 11版本之后的跨平台实现 (volatile)
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;
}
- 内存reorder 的解释:
m_instance = new Singleton();
(1) 正常情况是,a. 分配内存;b. 调用构造器,对a分配的内存进行初始化;c. 将初始化好的内存赋给m_instance 。(2)在指令级别,以上(1)中的三个步骤可能会 reorder,变成(A) 分配内存;(B)将(A)分配好的内存的地址给m_instance ;(C)执行构造器。这是编译器优化的工作,可能会出现的情况,这是C++,JAVA,C#都可能出现的情况。引起的后果是 t1 线程执行(A)(B)©三个步骤,在执行完 (B)步骤后,t2线程运行到判断 m_instance是否为空指针了,那此时不是空指针就直接返回,t2线程 需要返回的是构造器初始化后的指针,但实际上©步骤还没进行,t2线程获得的是一个没有经过初始化的指针,这会引发错误。 - volatile 关键字
享元模式 Flyweight
定义:运用共享技术有效的支持大量细粒度的对象。
- 场景举例:字体模块,每一种字体在全局只需要有一个实例。
class Font {
private:
//unique object key
string key;
//object state
//....
public:
Font(const string& 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(){
//...
}
};
- 基本思路:字体对象创建工厂,有一个对象池,每次创建新对象,都先检查,对象池中是否已经有这种对象了,如果已经有了就返回已创建好的对象,如果没有就构造器构建新对象,并把这个对象放入对象池,并返回这个对象。
- 注意对象创建好之后,就不能改变其状态了。
享元模式要点总结:
- 我们需要考虑对象的代价问题,Flyweight主要解决面向对象的代价问题。
- 采用对象共享的做法降低系统中对象的个数,从而降低细粒度给系统带来的内存压力。在具体实现方面,要注意对象状态的处理。
- 对象的数量太大从而导致对象内存开销加大,但是什么样的数量才算大呢?我们应该仔细的根据具体应用情况进行评估,不能凭空臆断。