摘要
单例模式(Single ton)与享元模式(Flyweight)归为对象性能模式。
1. 单例模式
顾名思义,单例就是单实例,有些类需要限定在一个进程内只能存在一个实例。举个例子:一款游戏需要处理键鼠消息,我们设计了一个类封装了所有的方法,那这个类有且只能创建唯一的一个对象来处理用户的键鼠信息。
单例模式的实现如果只考虑单线程的话so easy!但是涉及到多线程,难度一下就上来了。
#include <mutex>
/方案一 单线程版本/
/// <summary>
/// 单例类 可以把这个改成基类,子类继承来实现纯虚函数 getInstance,以提高代码复用
/// 最好的方法是新增泛型参数来实现服用
/// </summary>
class Singleton{
private:
Singleton();
Singleton(const Singleton& other);
public:
static Singleton* getInstance();
static Singleton* m_instance;
};
Singleton* Singleton::m_instance=nullptr;
// 线程非安全版本
// 何为线程非安全?
// 就是多线程cpu,存在A线程和B线程并发执行,同时new的话,就会对同一个静态变量m_instacnce进行写
Singleton* Singleton::getInstance() {
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
/方案二 多线程版本//
// 线程安全版本,但锁的代价过高,因为一些读操作都加锁了
// 如何保证线程安全?
// 简单的方法是创建临界区,就是加锁
class Singleton {
private:
Singleton();
Singleton(const Singleton& other);
public:
static Singleton* getInstance();
static Singleton* m_instance;
static std::mutex m_mutex;
};
Singleton* Singleton::getInstance() {
std::unique_lock<std::mutex> lock(Singleton::m_mutex);
lock.lock();//加锁之后也要判空,因为存在多线程同时执行到改行!所以里面的if判断必须要有
if (m_instance == nullptr)
{
m_instance = new Singleton();
}
lock.unlock();
return m_instance;
}
// ****以下的代码是不能用的,因为双检查锁有个编译器bug,即new可能先返回地址,构造器后构造对象,这样的话其他线程可能拿到
// 无法用的对象。因此如果是C#语言则需要加volatile关键字关闭编译器reorder
Singleton* Singleton::getInstance() {
std::unique_lock<std::mutex> lock(Singleton::m_mutex);
if (m_instance == nullptr) {
lock.lock();//加锁之后也要判空,因为存在多线程同时执行到改行!所以里面的if判断必须要有
if (m_instance == nullptr)
{
m_instance = new Singleton();
}
lock.unlock();
}
return m_instance;
}
///方案三 多线程跨平台版本/
class Singleton {
private:
Singleton();
Singleton(const Singleton& other);
public:
static Singleton* getInstance();
static std::atomic<Singleton*> m_instance;//实例是原子类里的一个对象
static std::mutex m_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);//内存栅栏是acuire模式,也就是内存的原子操作顺序限制为:先store后store
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);
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
以上代码的方案三需要编译器支持C++11,涉及原子操作,memory_order参数,若有不懂的可以参考这篇文章:链接: link
2. 享元模式
享元英文单词是flyweight,翻译过来是轻量级的意思,解决的是对象复用的问题,比如文档处理软件,如果里面的每个字都是一个对象的话,肯定有很多的重复对象,如何做到对象复用就是该模式解决的问题。
#include <map>
#include <string>
using namespace std;
class Font {
private:
//unique object key
string key;
//object state
//....
public:
Font(const string& key){
//...
}
void Draw()
{
//该字体对应的绘制函数
};
};
/// <summary>
/// FontFactory 这个类管理程序中的所有Font类对象,如果要用到Font类对象,则在这个map里面查就可以了,如果没查到,则会自动新建一个。
/// 引出的思考:
/// 1、Font对象如果使用过程中发生了修改怎么办?难道要更新map?
/// 2、Font的构造函数如果很复杂,用这种模式是否还适合?
/// 3、多线程访问是不安全的吧?
/// </summary>
class FontFactory{
private:
map<string,Font* > fontPool;
public:
Font* GetFont(const string& key)
{
std::map<string,Font*>::iterator item=fontPool.find(key);
if( item!= fontPool.end() )
{
return item->second;
}
else{
Font* pFont = new Font(key);
fontPool[key]= pFont;
return pFont;
}
}
void clear(){
//...
}
};