对象性能模式
面向对象很好的解决了抽象的问题,但是不可避免地要付出一定地代价。(比如虚函数 需要内存)对于通常情况下,面向对象地成本大都可以忽略不计。但是某些情况下,面向对象所带来地成本需要谨慎处理。
解决的不是抽象性的问题,解决的是代价问题
- 单例模式Singleton
- 享元模式Flyweight
1.单例模式Singleton
动机: 在软件系统中,经常有这样地一些特殊地类,必须保证它们在系统中只存在一个实例,才能确保它们地逻辑正确性、以及良好地效率
如何绕过常规地构造器,提供一种机制来保证一个类只有一个实例?
这应该是设计者的责任,而不是使用者的责任(需要确保使用者多次创建实例也只能得到一个实例)
class Singleton{
private:
Singleton();
Singleton(const Singleton& other);//copy构造函数设置成私有的
public:
static Singleton* getInstance();
static Singleton* m_instance;
};
Singleton* Singleton::m_instance=nullptr;
//线程非安全版本
//单线程ok 多线程不安全 多个线程同时判断则都创建新的实例
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不安全
//加锁后也需要重新判断指针变量是否为空
//reorder:指令序列通常认为会按顺序执行 但是在汇编层次 可能跟顺序不一样
//m_instance = new Singleton(); 如果拆分为3个步骤?1.分配内存 2.调用构造器初始化 3.将得到的指针给m_instance
//真正在cpu指令级别 可能顺序发生变化. 132
//reorder后,当threadA执行了13之后 threadB执行时m_instance非空 直接返回 但是返回的是还没有构造完成的
Singleton* Singleton::getInstance() {
if(m_instance==nullptr){
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}
//C++ 11版本之后的跨平台实现 (java和c#加了一个关键字volatile 加了之后不能reorder)
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
//下面两行实现了命令的order
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;
}
模式定义: 保证一个类仅有一个实例,并提供一个该实例的全局访问点
要点总结:
- Singleton模式中的实例构造器可以设置为protected允许子类派生
- Singleton模式一般不要支持copy构造函数和clone接口,因为这有可能导致多个对象实例,与Singleton模式初衷违背
- 如何实现多线程环境安全的Singleton模式?注意对双检查锁的正确实现
2.享元模式Flyweighe
动机: 在软件系统采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价——主要指内存需求方面的代价
如何在避免大量细粒度对象问题的同时,让外部客户程序仍然能够透明地使用面向对象地方式来进行操作?
一切皆对象合理吗?一个int型类型地数据当作对象 没必要 如果将所有地东西都实现为对象 那么一些小地常用地东西当作对象会带来很大地内存代价
//字处理系统
//字体对象 如果一篇文章为每一个字符串创建一个字体对象 不合理
//一篇十几页的文章用到的字体一般就3.4种而已
//需要共享地对象创建完应该是只读地
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(){
//...
}
};
模式定义: 运用共享技术有效地支持大量细粒度地对象
比如字符串,在系统中用的很多 stl中地string都用了共享系统 线程池也是会用到共享技术
要点总结:
- 面向对象很好的解决了抽象性的问题,但是作为一个运行在机器种的程序实体,我们需要考虑对象的代价问题。Flyweight主要解决面向对象的代价问题,一般不触及面向对象的抽象性问题
- Flyweight采用对象共享的做法来降低系统中对象的个数,从而降低细粒度对象给系统带来的内存压力 在具体实现方面要注意对象状态的处理
- 对象的数量太大从而导致对象内存开销加大——什么样的数量才算大?这需要我们仔细地根据具体应用情况进行评估,而不能凭空臆断