288-C++设计模式(单例模式)

设计模式概念

设计模式简单来说就是在解决某一类问题场景时,有既定的,优秀的代码框架可以直接使用,与我们自己摸索出来的问题解决之道相比较,有以下优点可取:

  1. 代码更易于维护,代码的可读性,复用性,可移植性,健壮性会更好;
  2. 当软件原有需求有变更或者增加新的需求时,合理的设计模式的应用,能够做到软件设计要求的“开-闭原则”,即对修改关闭,对扩展开放,使软件原有功能修改,新功能扩充非常灵活;
  3. 合理的设计模式的选择,会使软件设计更加模块化,积极的做到软件设计遵循的根本原则“高内聚,低耦合

因此掌握常用的设计模式非常有必要,无论是找工作,还是对于我们自己的项目软件设计,都很有帮助。

单例模式

1、单例模式简介

一个类不管创建多少次对象,永远只能得到该类类型的唯一一个实例对象;

那么设计一个单例就必须要满足下面三个条件:

  • 1、构造函数私有化,这样用户就不能任意定义该类型的对象了;
  • 2、定义该类型唯一的对象;
  • 3、通过一个static静态成员方法返回唯一的对象实例;(不能写成普通成员变量,因为普通成员方法调用还需要依赖对象!)
  • 4、 拷贝构造赋值重载都要删掉

单例模式分类:

  1. 饿汉式单例模式: 还没有获取实例对象,实例对象就已经产生了;是线程安全的
  2. 懒汉式单例模式: 唯一的实例对象,直到第一次获取它的时候,才产生;需要考虑线程安全的问题;

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指针赋值;

那么在多线程环境下,就有可能出现如下问题:

  1. 线程A先调用getInstance函数,由于instance 为nullptr,进入if语句
  2. new操作先开辟内存,此时A线程的CPU时间片到了,切换到B线程
  3. 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;
}

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liufeng2023

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值