文章目录
面向对象很好地解决了“抽象” 的问题,但是必不可免地要付出一定的代价。 对于通常情况来讲,面向对象的成本大都可以忽略不计。 但是某些情况,面向对象所带来的成本必须谨慎处理。本节在设计模式中解决的是性能问题,而不是抽象问题。
1. Singleton 单例模式
1.1 Singleton 单例模式动机
在软件系统中,经常会有一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性以及良好的效率;
那么如何绕过常规的构造器(默认构造,拷贝构造,拷贝赋值),提供一种机制来保证一个类只有一个实例?
1.2 模式定义
以上对于一个类只有一个实例的保证,应当由类设计者来实现,而不是使用者的责任去保证只有一个实例存在。
保证一个类仅有一个实例,并提供一个该实例的全局访问点。
![](https://i-blog.csdnimg.cn/blog_migrate/d3a653ab40f1c24bcbe9dd307d47df77.png)
这里有一篇博文,虽是 java 的,但是有参考价值:设计模式——单例模式(java:一步步优化至最优写法)
1.3 示例代码
假设需要一个类名为 Singleton,业务要求只能有一个 Singleton 类型的实例对象,下面实例代码提供了Singleton 类的定义式,和其中静态成员函数 getInstance() 的多个实现版本,围绕线程安全问题展开。
class Singleton{
private:
//将构造函数和拷贝构造函数显式声明为private的
//对于拷贝赋值operator=,其调用拷贝构造韩式,可以不管
//实例构造器也可声明为protected的,以供子类进行派生
Singleton();
Singleton(const Singleton& other);
public:
//对外提供访问访问该类单一实例的接口 getInstance()
static Singleton* getInstance();
//将实例声明为static的,同时也可声明为private的(可选)
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() {
//对于资源的读操作可以不进行加锁操作
//若是对于m_instace进行实例化就需要加锁了
//该版本是无论是否进行写操作都进行加锁,绝对安全但是效率不高
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {
//双检查锁,其意义是,若是线程A和线程B同时进行并同时通过了第一个if判定,那么:
//假设若A先拿到锁,那么在A将锁归还后,B仍会进行取锁,实例化Singleton的操作
//导致了两个实例化对象的创建生成,所以第二个if判定是为了杜绝以上情况的发生。
//对于内存读写reorder不安全,是因为cpu在读写内存时,实际指令的顺序与我们预想的顺序不一定一致
//例如,对于m_instance = new Singleton()这句新建对象,
//我们预想顺序为:1.分配内存;2.调用Singleton()构造函数;3.将得到的内存地址赋值给m_instance
//但是在实际过程中可能顺序为:1->3->2,这时,所获取的指针指向的内存并无内容
//所以该版本对于内存读写reorder是不安全的,因为存在内存内指令取用的指令重排问题。
if(m_instance==nullptr){
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}
//C++ 11版本之后的跨平台实现 (volatile)
//调用c++11 std中提供的类库,atomic表示原子性,mutex表示互斥
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;
}
1.4 要点总结
- Singleton 模式中的实例构造器一般设置为 private 的,也可以设置为 protected 以允许子类派生;
- Singleton 模式一般不要支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例,与 Singleton 模式初衷违背;
- 在实现多线程环境下安全的 Singleton,须注意对双检查锁的正确实现,主要是对指令顺序的 reorder 问题,使用 violate 关键字实现;
2. Flyweight 享元模式
2.1 Flyweight 享元模式动机
在软件系统中采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价——主要指内存需求方面的代价。
那么如何避免大量细粒度问题的同时,让外部客户程序仍然能够透明地使用面向对象的方式来进行操作?
2.2 模式定义
运用共享技术有效地支持大量细粒度对象。
个人理解这里地大量细粒度对象是指多个线程/函数都会使用到的相同的具化对象,且无须进行更改(意味着是种可复用的只读对象)。那么可以将这种具化对象只创建出一个,放入资源池中,若是需要取用即可,这样可以裁剪掉大量的重复对象,也即是共享技术/享元模式的思想。
![](https://i-blog.csdnimg.cn/blog_migrate/93b46f1b630e05c74fb0404c9de5e45d.png)
这里的 UML 图比较复杂,但只是将 Flyweight 资源套用了 Factory Method 模式而已,Flyweight 就是客户所需具化的 Product 抽象基类,若是 UnsharedConcreteFlyweight,则与 Factory Method 模式中的 ConcreteProduct 别无二样(工厂模式见:★1. Factory Method 工厂模式)。当然,这里最主要的内容是对于 ConcreteFlyweight 的复用与共享。
2.3 示例代码
示例场景对应的是文档中的字体,假设一个文档中要求完稿需要多种字体。当有多段相同内容的string,且这些string的字体是同一个时,会造成许多相同字符串对象的创建,进而造成不必要的内存开销。这时,享元模式就可以缓解这一问题。
//对应UML中的Flyweight,但不是抽象基类,所以没有使用工厂模式
class Font {
private:
//unique object key
string key;
//object state
//....
public:
Font(const string& key){
//...
}
};
class FontFactory{
private:
//内建一map容器,将字符串和字体进行映射对应
map<string,Font* > fontPool;
public:
Font* GetFont(const string& key){ //对应 GetFlyweight(key) 函数
//没有使用<algorithm>中的find函数进行键值查找,而是使用map自身提供的find函数,效率更高,针对RBTree
map<string,Font*>::iterator item=fontPool.find(key);
//若是找到了该字体,返回该对象示例
if(item!=footPool.end()){
return fontPool[key];
}
else{ //若是没有找到,则创建新的实例对象,再返回
//这里简化了,没有使用工厂模式,而是直接使用new进行动态绑定
Font* font = new Font(key);
fontPool[key]= font;
return font;
}
}
void operation() {
//...
}
//...
};
2.4 要点总结
- 面向对象很好地解决了抽象性的问题,但是作为一个运行在机器中的程序实体,我们需要考虑对象的代价问题。Flyweight 主要解决面向对象的代价问题,一般不触及面向对象的抽象性问题;
- Flyweight 采用对象共享的做法来降低系统中对象的个数,从而降低细粒度给系统带来的压力。在具体实现方面,要注意对象状态的处理(对象应当是只读的,不进行修改操作);
- 对象的数量太大从而导致对象内存开销过大——那么什么样的数量才是大?需要根据 ①类的大小(见:C++类空间大小),和 ②程序运行时具体会保有多少对象 来进行评估,而不是凭空臆断;