设计模式概念
设计模式简单来说就是在解决某一类问题场景时,有既定的,优秀的代码框架可以直接使用,与我们自己摸索出来的问题解决之道相比较,有以下优点可取:
- 代码更易于维护,代码的可读性,复用性,可移植性,健壮性会更好;
- 当软件原有需求有变更或者增加新的需求时,合理的设计模式的应用,能够做到软件设计要求的“开-闭原则”,即对修改关闭,对扩展开放,使软件原有功能修改,新功能扩充非常灵活;
- 合理的设计模式的选择,会使软件设计更加模块化,积极的做到软件设计遵循的根本原则“高内聚,低耦合”
因此掌握常用的设计模式非常有必要,无论是找工作,还是对于我们自己的项目软件设计,都很有帮助。
单例模式
1、单例模式简介
一个类不管创建多少次对象,永远只能得到该类类型的唯一一个实例对象;
那么设计一个单例就必须要满足下面三个条件:
- 1、构造函数私有化,这样用户就不能任意定义该类型的对象了;
- 2、定义该类型唯一的对象;
- 3、通过一个static静态成员方法返回唯一的对象实例;(不能写成普通成员变量,因为普通成员方法调用还需要依赖对象!)
- 4、 拷贝构造和赋值重载都要删掉;
单例模式分类:
- 饿汉式单例模式: 还没有获取实例对象,实例对象就已经产生了;是线程安全的
- 懒汉式单例模式: 唯一的实例对象,直到第一次获取它的时候,才产生;需要考虑线程安全的问题;
2、单例模式的应用场景
- 日志模块
- 数据库模块(数据库客户端和服务端进行通信,需要通过数据库模块进行通信,将数据库模块设计成一个单例,客户端和服务端进行通信时,客户端可以调用这一个数据库模块对象的某一个方法,不需要创建很多对象!)
3、饿汉式单例模式
饿汉式单例模式: 还没有获取实例对象,实例对象就已经产生了;线程安全的
静态类对象在内存的数据段;(创建和初始化是在main函数之前完成的,所以上面写的是一个饿汉式单例模式;)
优点: 一定是线程安全的(静态对象在main函数之前就已经初始化好了)
缺点: 对象的构造会调用构造函数,实际上项目上的构造函数会做很多事情,应该需要对象时才创建,万一没有用到,就会造成资源浪费。
所以懒汉式单例模式可能更受欢迎。(将对象的实例化,延迟到第一次获取该对象时,对象才初始化;)
#include<iostream>
using namespace std;
class CSingleton
{
public:
static CSingleton* getInstance()//# 3、获取类的唯一实例对象的接口方法
{
return &single;
}
private:
static CSingleton single;//# 2、定义一个唯一的类的实例对象
CSingleton() { cout << "CSingleton()" << endl; }//# 1、构造函数私有化
~CSingleton() { cout << "~CSingleton()" << endl; }
CSingleton(const CSingleton& ) = delete;
CSingleton& operator=(const CSingleton&) = delete;
};
CSingleton CSingleton::single;
int main()
{
CSingleton* p1 = CSingleton::getInstance();
CSingleton* p2 = CSingleton::getInstance();
CSingleton* p3 = CSingleton::getInstance();
cout << p1 << " " << p2 << " " << p3 << endl;
return 0;
}
4、懒汉式单例模式1
将对象的实例化,延迟到第一次获取该对象时,对象才初始化;
-
程序启动时,数据段上只有一个指针,没有任何对象创建;
-
然后调用获取单例对象方法时,会new一个对象。
#include<iostream>
using namespace std;
class CSingleton
{
public:
static CSingleton* getInstance()//# 3、获取类的唯一实例对象的接口方法
{
if (nullptr == instance)
{
instance = new CSingleton();
}
return instance;
}
private:
static CSingleton* instance;//# 2、定义一个唯一的类的实例对象
CSingleton() { cout << "CSingleton()" << endl; }//# 1、构造函数私有化
~CSingleton() { cout << "~CSingleton()" << endl; }
CSingleton(const CSingleton& ) = delete;
CSingleton& operator=(const CSingleton&) = delete;
};
CSingleton* CSingleton::instance = nullptr;
int main()
{
CSingleton* p1 = CSingleton::getInstance();
CSingleton* p2 = CSingleton::getInstance();
CSingleton* p3 = CSingleton::getInstance();
cout << p1 << " " << p2 << " " << p3 << endl;
return 0;
}
问题:懒汉式单例模式是不是线程安全的?
可重入函数:
- 一个函数还没有执行完,能不能再调用一次?(单线程下是不可能的,多线程下是可以的!)
- 如果函数可以在多线程下运行,且不会出现竞态条件,这个函数就是可重入函数!
- 如果在多线程下运行,会发生竞态条件,函数就不是可重入函数,需要考虑线程安全的问题!
很明显,这个getInstance是个不可重入函数,也就它在多线程环境中执行,会出现竞态条件问题;
首先搞清楚这句代码,instance = new CSingleton();
它会做三件事情,开辟内存,调用构造函数,给single指针赋值;
那么在多线程环境下,就有可能出现如下问题:
- 线程A先调用getInstance函数,由于instance 为nullptr,进入if语句
- new操作先开辟内存,此时A线程的CPU时间片到了,切换到B线程
- B线程由于instance 为nullptr,也进入if语句了,开始new操作
很明显,上面两个线程都进入了if语句,都试图new一个新的对象,不符合单例模式的设计,那该如何处理呢?
- 应该为getInstance函数(相当于临界区代码段,一定要保证原子操作!)内部加锁,在线程间进行互斥操作。
使用多线程的锁的时候,加入头文件 #include
锁得粒度太大了,单线程的环境下也要频繁的加锁和解锁!
修改:
这样在单线程下就没有问题了!
但是多线程还是存在问题:
- 线程1和线程2都进入if函数,线程1获得了互斥锁,线程2阻塞在获取锁的语句上;
- 当线程1获取对象出函数释放锁后,线程2又会构造一个对象,不可以!
多线程会出现问题,需要 锁+双重判断
此时就是一个线程安全的懒汉式单例模式!
注意:
-
instance指针是在数据段的,同一进程多个线程共享的内存;
-
cpu在执行线程指令时,为了加快指令执行,会让线程将共享内存的值都拷贝一份,放到自己的线程的缓存中,放到cpu的缓存中;
-
instance还需要加一个volatile关键字,是给指针加的(不是给指针的指向加的);
-
好处: 当一个线程对instance赋值时,其他线程马上均能看到instance的改变。因为线程现在已经不对共享变量进行缓存了,大家看的都是原始的,都是内存中的值;
#include<iostream>
#include <mutex>
using namespace std;
std::mutex mtx;
class CSingleton
{
public:
static CSingleton* getInstance()//# 3、获取类的唯一实例对象的接口方法
{
//lock_guard<std::mutex>guard(mtx);//锁的粒度太大了
if (nullptr == instance)
{
lock_guard<std::mutex>guard(mtx);
if (nullptr == instance)
{
instance = new CSingleton();
}
}
return instance;
}
private:
static CSingleton* volatile instance;//# 2、定义一个唯一的类的实例对象
CSingleton() { cout << "CSingleton()" << endl; }//# 1、构造函数私有化
~CSingleton() { cout << "~CSingleton()" << endl; }
CSingleton(const CSingleton& ) = delete;
CSingleton& operator=(const CSingleton&) = delete;
};
CSingleton* volatile CSingleton::instance = nullptr;
int main()
{
CSingleton* p1 = CSingleton::getInstance();
CSingleton* p2 = CSingleton::getInstance();
CSingleton* p3 = CSingleton::getInstance();
cout << p1 << " " << p2 << " " << p3 << endl;
return 0;
}
以上就是一个非常安全的线程安全带的懒汉式单例模式!
5、懒汉式单例模式2—简单实用的懒汉模式
也是线程安全的,而且还不需要互斥锁!非常精简!
instance是静态的局部变量,程序在启动阶段,该对象的内存就有了,内存在数据段上;
但是静态对象第一次初始化是第一次运行到它的时候,才会进行初始化(懒汉式单例模式),上面这种,如果没有调用getInstance函数,对象是不会构造的;
问题: 多线程下,线程1在调用getInstance函数,然后构造对象时,构造函数内容太多,处于执行状态;此时线程2又进来,发现没有单例对象,又进行构造了调用getInstance函数创建对象了。就是可能在多线程环境下有两个线程同时调用它,导致生成2个对象。
在Linux环境中,通过g++编译上面的代码,命令如下:
g++ -o main main.cpp -g
生成可执行文件main,用gdb进行调试,到getInstance函数,并打印该函数的汇编指令,如下:
可以看到,对于static静态局部变量的初始化,在汇编指令上可以看到,编译器会自动对它的初始化进行加锁和解锁控制,使静态局部变量的初始化成为线程安全的操作,不用担心多个线程都会初始化静态局部变量,因此上面的懒汉单例模式是线程安全的单例模式!
#include<iostream>
using namespace std;
class CSingleton
{
public:
static CSingleton* getInstance()//# 3、获取类的唯一实例对象的接口方法
{
static CSingleton instance;//# 2、定义一个唯一的类的实例对象
return &instance;
}
private:
CSingleton() { cout << "CSingleton()" << endl; }//# 1、构造函数私有化
~CSingleton() { cout << "~CSingleton()" << endl; }
CSingleton(const CSingleton&) = delete;
CSingleton& operator=(const CSingleton&) = delete;
};
int main()
{
CSingleton* p1 = CSingleton::getInstance();
CSingleton* p2 = CSingleton::getInstance();
CSingleton* p3 = CSingleton::getInstance();
cout << p1 << " " << p2 << " " << p3 << endl;
return 0;
}