设计模式:C++如何实现一个高质量的单例模式,双重校验锁 懒汉多线程安全 ,还得考虑防止编译器优化、异常死锁

       

目录

       

 前言:        

        一、作用

        二、实现

        三、分类

        四、实现方式

        1、采用指针的方式来保存全局对象

        1.1、指针饿汉:

        1.2、指针懒汉

        2、采用对象的方式来保存全局对象

如果还有更多补充的,可以在评论区一起讨论哈。


前言:        

        众所周知,设计模式有23种,其中单例模式是用得非常广泛的,也是很多人经常面试被问到的问题,当然工作中也是经常用到的设计模式,我们一起来聊聊单例模式。

一、作用

        单例模式能保证全局有且只有一个实例对象;

二、实现

        一般情况下,得控制对象的生成、释放,使其私有化,可以禁用或屏蔽掉一些构造函数、析构函数、拷贝构造函数、重载等号赋值函数等,使其不能被外部直接使用;同时为了保证全局唯一性,得提供一个静态成员变量来保存这个全局实例;为了能让使用者可以拿到对象,还得提供一个公有的静态函数,让使用者可以直接拿到对象。如果采用指针的方式来保存和创建对象,还得考虑添加一个释放函数,防止内存泄露。

        总结下来,实现需要处理三点:

        1、控制对象生成:私有化构造、析构、拷贝构造、重载等号赋值函数;

        2、私有的静态成员变量:保存对象

        3、公有的静态成员函数:可以生成对象并返回对象

三、分类

        由于需要使用到静态成员变量来保存对象,静态成员变量初始化的内容不同,就可以产生2种单例模式,分别为饿汉单例、懒汉单例;

        饿汉单例:即静态成员变量初始化就给内存,用空间换时间;意思是很饿,需要随时可以拿到吃的,顾名思义为饿汉模式;

        懒汉单例:即静态成员变量初始化为空,需要使用了再给内存,用时间换空间;意思是现在不饿,等饿了再拿,饿了再去做吃的,故为懒汉单例;

四、实现方式

      1、采用指针的方式来保存全局对象

        1.1、指针饿汉:

                这种实现方式,在饿汉单例里面,不需要考虑太多,直接返回对象指针即可;

#include 
#include 
using namespace std;

mutex g_mutex; // 全局互斥锁

class Singleton
{
private:
	Singleton(){}
	~Singleton(){}
	Singleton(const Singleton &t){}
	void operator=(const Singleton &t){}
	
	static Singleton *s_obj; // volatile 防止编译器优化,保证从原始内存读数据,而不是从寄存器读取
public:
	static Singleton* getInstance() {
		return s_obj;
	}
	void release() {
		if (s_obj) {
			delete s_obj;
			s_obj = nullptr;
		}
	}
};
Singleton* Singleton::s_obj = new Singleton; // 饿汉单例,空间换时间

int main()
{
	Singleton *a = Singleton::getInstance();
	Singleton *b = Singleton::getInstance();
	cout << a << " " << b << endl;
	if (a == b) {
		cout << "单例成功" << endl;
	}
	else {
		cout << "单例失败" << endl;
	}
	a->release();
	b->release();
    return 0;
}

        1.2、指针懒汉

                这种实现方式,就得考虑多线程安全,还得考虑防止编译器优化,还得防止异常死锁

安全问题:加上互斥锁即可解决

异常死锁:由于在new的过程中,极有可能会导致异常退出,使得互斥锁没有来得及退出导致的死锁问题,可以考虑使用唯一锁来配合互斥锁处理异常死锁;

性能问题:加上互斥锁之后,每次都要上锁和解锁的话,还是比较费时间的,为了提升性能,可以对指针先判空处理,不为空了,直接返回对象指针了,不需要再去上锁解锁了,大大节约了时间,提升性能;

关键字volatile的神奇之处:由于在多线程new对象的过程中,有可能会在初始化过程中,产生了地址,但是其他线程从寄存器拿到地址去操作去了,为了防止其他线程没有从原始内存中拿数据,加上一个volatile关键字来修饰这个指针,可以起到很好的效果;

代码如下:

#include 
#include 
using namespace std;

mutex g_mutex; // 全局互斥锁

class Singleton
{
private:
	Singleton(){}
	~Singleton(){}
	Singleton(const Singleton &t){}
	void operator=(const Singleton &t){}
	
	static Singleton * volatile s_obj; // volatile 防止编译器优化,保证从原始内存读数据,而不是从寄存器读取
public:
	static Singleton* getInstance() {
		// 双重校验锁:保证安全又能提升性能
		if (s_obj == nullptr) { // 提升性能,有内存之后,不会再进去上锁解锁
			unique_lock ul(g_mutex); // 防止异常死锁
			//g_mutex.lock();
			if (s_obj == nullptr) { // 防止多次new导致的内存泄露
				s_obj = new Singleton;
			}
			//g_mutex.unlock();		
		}
		return s_obj;
	}
	void release() {
		if (s_obj) {
			delete s_obj;
			s_obj = nullptr;
		}
	}
};
//Singleton* Singleton::s_obj = new Singleton; // 饿汉单例,空间换时间
Singleton* volatile Singleton::s_obj = nullptr; // 懒汉单例,时间换空间
int main()
{
	Singleton *a = Singleton::getInstance();
	Singleton *b = Singleton::getInstance();
	cout << a << " " << b << endl;
	if (a == b) {
		cout << "单例成功" << endl;
	}
	else {
		cout << "单例失败" << endl;
	}
	a->release();
	b->release();
    return 0;
}

      2、采用对象的方式来保存全局对象

        直接在静态函数中,使用一个静态局部变量来保存对象,就会很有意思,可以直接返回对象地址。

#include 
using namespace std;

class Singleton
{
private:
	Singleton() {}
	~Singleton() {}
	Singleton(const Singleton &t) {}
	void operator=(const Singleton &t) {}

public:
	static Singleton * getInstance() {
		static Singleton obj;  // 静态局部变量,存放静态存储区
		return &obj;
	}
};
int main()
{
	Singleton *a = Singleton::getInstance();
	Singleton *b = Singleton::getInstance();
	cout << a << " " << b << endl;
	if (a == b) {
		cout << "单例成功" << endl;
	}
	else {
		cout << "单例失败" << endl;
	}
	return 0;
}

如果还有更多补充的,可以在评论区一起讨论哈。

你可能感兴趣的:(C++随想录,设计模式,c++,单例模式,安全,开发语言,数据结构)