对象性能模式介绍
面向对象可以较好的解决抽象问题,但是面向抽象相比于面向过程会有额外的开销,虽然这种开销在大多数情况下都可以忽略不计。但是在某些情况下,面向对象造成的开销必须谨慎处理。23种设计模式种关于对象性能的设计模式有Singlton模式和Flyweight模式。
单件模式(Singlton)
使用场景
在程序中,有一些类只能有一个实例才能保证逻辑的正确性以及良好的效率,比如说数据库类,只能存在一个正在连接的实例,如果存在多个正在连接的实例则会导致数据库连接异常。或者在web服务器种,客户端同时向服务器的数据库请求访问,这就会导致数据库出现会话阻塞。
代码
如何提供一种机制保证一个类只有一个实例?
class Singleton
{
private:
// 为什么构造函数和拷贝构造函数需要放在private里面?而不是放在public里面
/*
*因为Singleton是单件模式,他要求整个程序只能有一个类的实体
如果把构造函数放在public里面那么系统内部就可以创建该实体
这就不符合单件模式的要求
*/
// 构造函数
Singleton();
// 拷贝构造函数
Singleton(const Singleton& other);
public:
static Singleton* getInstance();
static Singleton* m_instance;
};
Singleton* Singleton::m_instance = nullptr;
// 线程非安全版本
Singleton* Singleton::getInstance()
{
if (m_instance == nullptr)
{
m_instance = new Singleton();
}
return m_instance;
}
/*
* 上述代码对于单线程是没有问题的,但是在多线程中则会有问题。
假设现在有两个进程在上面的程序中
A线程将要执行m_instance = new Singleton();这个语句
此时,B线程执行了if(m_instance == nullptr)这个语句
由于A线程还没有执行m_instance = new Singleton()
B线程认为m_instance还为nullptr,于是顺利的进入了if语句的内部。
这就导致了A和B分别创建了两个实例
*/
//单检查锁,线程安全但是消耗高
Singleton* Singleton::getInstance()
{
//上锁,放置一个锁的局部变量
//由于局部变量会在函数结束的时候消失
//因此,如果这个函数执行结束也就自动的解锁
Lock lock;
if (m_instance == nullptr)
{
m_instance = new Singleton();
}
return m_instance;
}
/*锁怎么设计呢?这个问题我还不知道
* 上面的代码为什么说安全但是消耗高呢
* 假设程序已经创建了实例,此时有2个线程A和B
* A上锁该函数并且将要执行 if (m_instance == nullptr)这个语句
* 此时B也进入了函数,但是B只想要读取该实例,但是由于A已经对该函数上锁,B就无法读取该实例
* 也就是说,当程序只是想要读取实例的时候,上锁会导致读取变慢,但是实际上读取是不会创建实例的,对该读取操作上锁是没有必要的
*/
// 双检查锁,但是由于内存读写reorder会导致不安全
Singleton* Singleton::getInstance()
{
if (m_instance == nullptr)
{
Lock lock;
if (m_instance == nullptr)
{
m_instance = new Singleton();
}
}
return m_instance;
}
/*上面的代码的逻辑是:
* 首先检查这个实例是否为空
* 如果是空则代表要创建一个实例,就需要加锁
* 随后再次判断该实例是否为空,为什么要再次判断呢?
* 因为如果没有这个判断语句,代码就变成了
* if (m_instance == nullptr)
{
Lock lock;
m_instance = new Singleton();
}
return m_instance;
现在假设有2个进程A和B,现在A将要执行Lock lock;这个语句
此时B进入了该函数,发现实例为空于是也要执行Lock lock语句。
但是由于A上锁了,B必须等到A执行完才能继续执行
于是B等到A执行完return m_instance;后发现函数已经解锁于是也执行完了return m_instance
这就导致了A和B创建了两个实例。
* 如果这个实例依然为空则代表没有别的进程正在创建该实例就可以安心的创建实例
*上述代码的逻辑没有问题
*但是在实际的编译中,上述的语句会被编译成一条条的cpu指令
*而某些编译器会基于优化对cpu指令的执行顺序稍作修改
*比如说 m_instance = new Singleton();这个语句,可以分为开辟地址空间->地址空间写入数据->把地址传给变量三个部分
*但是由于编译器的优化,操作就转变成了开辟地址空间->把地址赋值给变量->写入数据
* 假设A进程执行了m_instance = new Singleton();并且cpu的指令操作为开辟地址空间->把地址赋值给变量->写入数据
* 在A进程将要执行指令写入数据时,进程B抢到了cpu的控制权,执行了语句if (m_instance == nullptr)发现不成立,于是返回了这个实例
* 但是这个实例只分配了内存空间并没有写入数据,这就会导致内存错误。
*/
/*解决方法
* 这是编译器需要解决的事情,c++11版本后有了volatile关键字,这个关键字可以要求编译器不对执行的语句进行优化,这就顺利解决了reoder问题
*/
int main()
{
}