C++面向对象基础:设计模式(中)-- 单例模式详解

结构型设计模式

单例模式

保证一个类仅有一个实例,并提供一个该实例的全局访问点。 ——《设计模式》

这个设计模式的定义实在是简单明了,也是最常用的设计模式,用该设计模式可以定义一个类仅会有一个实例对象,并且可以全局访问。

示例代码

单例模式版本一
lass Singleton {
public:
	static Singleton * GetInstance() {
		if (_instance == nullptr) {
			_instance = new Singleton();
		}
		return _instance;
	}
private:
	Singleton() {} //构造
	~Singleton() {}
	Singleton(const Singleton &clone){} //拷⻉构造
	Singleton& operator=(const Singleton&) {}
	static Singleton * _instance;
};

Singleton* Singleton::_instance = nullptr;//静态成员需要初始化

这里将各种构造函数和operator=都定义为私有成员来保证全局只有一个实例,使用static变量来保存唯一实例,并提供static方法来访问该实例。

代码结构
  1. 私有的构造和析构
  2. 禁掉拷贝构造,拷贝赋值,移动构造,移动赋值的操作
  3. 静态类成员函数
  4. 静态私有成员变量
问题

因为析构函数被置为私有,这个实现中我们没办法释放_instance的内存,实际出现了内存泄漏(可以将原始指针修改为unique_ptr型的智能指针来解决)。同时并不能做到线程安全。

单例模式版本二
class Singleton {
public:
	static Singleton * GetInstance() {
		if (_instance == nullptr) {
			_instance = new Singleton();
			atexit(Destructor);
		}
		return _instance;
	}
	
	~Singleton() {}
private:
	static void Destructor() {
		if (nullptr != _instance) { //
			delete _instance;
			_instance = nullptr;
		}
	}
	
	Singleton();//构造
	~Singleton() {}
	
	Singleton(const Singleton &cpy); //拷⻉构造
	Singleton& operator=(const Singleton& other) {}
	static Singleton * _instance;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化

使用atexit函数注册一个用于程序退出时,释放_instance内存的函数,使用这种设计解决内存泄漏的问题。

问题

虽然不存在内存泄漏问题,但此时的版本并非线程安全,多个线程同时调用GetInstance可能会使得new操作同时执行多次,这不是我们希望的。

单例模式版本三
class Singleton {
public:
	static Singleton * GetInstance() {
		//使用双重检测去避免线程安全	
		//可能1
		if (_instance == nullptr){
			//可能2
			std::lock_guard<std::mutex> lock(_mutex);
			if (_instance == nullptr) {
				_instance = new Singleton();
				atexit(Destructor);
			}
		}
		return _instance;
	}
	
	~Singleton() {}
private:
	static void Destructor() {
		if (nullptr != _instance) { //
			delete _instance;
			_instance = nullptr;
		}
	}
	
	Singleton();//构造
	~Singleton() {}
	
	Singleton(const Singleton &cpy); //拷⻉构造
	Singleton& operator=(const Singleton& other) {}
	static Singleton * _instance;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化

这里通过加锁的方式,“解决了”内存泄漏的问题,注意在GetInstace中,我们使用了双重if语句进行判断,下面解析一下这样的设计。
还记得我们想解决的线程安全问题是什么吗?防止在多个线程中对反复执行了new操作。在本次的版本中,当一个线程持有锁后,其他的线程只能处于注释中标记的可能1和可能2两个位置,如果没有第二个if语言判断,易观察到处于可能2的线程依旧会执行到new语句,这亦不是我们希望的。

问题

虽然这个代码在逻辑上貌似已经解决了线程安全的问题,但实际上,在多线程环境下cpu指令重排的情况下,依旧可能出现线程安全,分析如下:
正常来说new操作符的内部执行分为以下几步:
1. 分配内存
2. 调用构造函数
3. 返回指针

但是由于cpu指令重排的可能,new的执行流程可能会变成1->3->2。此时可能会出现严重的内存安全问题。
当持有锁的线程执行到new操作时,若其仅仅执行到1->2但还并未调用构造函数,此时会出现这样的情况:_instance指针已经拿到了new的返回值,此时已经不是nullptr,但因为还没有调用构造函数此时的_instance还没有实际的数据,而处于其他地方的线程可能会在这个时候直接将_instance返回并使用,这会导致严重的线程安全问题,这绝不是我们希望发生的。

单例模式版本四
#include <mutex>
#include <atomic>
class Singleton {
public:
	static Singleton * GetInstance() {
		Singleton* tmp = _instance.load(std::memory_order_relaxed);
		std::atomic_thread_fence(std::memory_order_acquire);//获取内存屏障
		if (tmp == nullptr) {
			std::lock_guard<std::mutex> lock(_mutex);
			tmp = _instance.load(std::memory_order_relaxed);
			if (tmp == nullptr) {
				tmp = new Singleton;
				std::atomic_thread_fence(std::memory_order_release);//释放内存屏障
				_instance.store(tmp, std::memory_order_relaxed);
				atexit(Destructor);
			}
		}
		return tmp;
	}
	
private:
	static void Destructor() {
		Singleton* tmp = _instance.load(std::memory_order_relaxed);
		if (nullptr != tmp) {
			delete tmp;
		}
	}

	Singleton(){}	
	Singleton(const Singleton&) {}
	Singleton& operator=(const Singleton&) {}
	static std::atomic<Singleton*> _instance;
	static std::mutex _mutex;
};
std::atomic<Singleton*> Singleton::_instance;//静态成员需要初始化
std::mutex Singleton::_mutex; //互斥锁初始化

使用内存栅栏,使得new操作符的操作不会影响到 if 语言判断之前,通过无锁编程,解决了线程安全的问题。

单例模式版本五
class Singleton
{
public:
	static Singleton& GetInstance() {
		static Singleton instance;
		return instance;
	}
private:
	Singleton(){}
	~Singleton() {}
	Singleton(const Singleton&) {}
	Singleton& operator=(const Singleton&) {}
};

相对于版本四,这个版本的代码可谓十分简洁,借助于static关键字的线程安全性,该版本保证了没有内存泄漏和不存在线程安全问题,其析构函数也可以正常调用到。

单例模式版本六
template<typename T>
class Singleton {
public:
	static T& GetInstance() {
		static T instance; // 这⾥要初始化DesignPattern,需要调⽤DesignPattern构造函数同时会调⽤⽗类的构造函数。
		return instance;
	}
protected:
	virtual ~Singleton() {}
	Singleton() {} // protected修饰构造函数,才能让别⼈继承
	Singleton(const Singleton&) {}
	Singleton& operator =(const Singleton&) {}
};

class DesignPattern : public Singleton<DesignPattern> {
	friend class Singleton<DesignPattern>; // friend 能让Singleton<T> 访问到DesignPattern构造函数
public:
	~DesignPattern() {}
private:
	DesignPattern() {}
	DesignPattern(const DesignPattern&) {}
	DesignPattern& operator=(const DesignPattern&) {}
};

相对于版本五,这个版本的单例模式设计可以让我们更加方便的进行扩展,我们希望将什么类型设置为单例模式,只需要让这个类继承于Singleton的对应模板并将其设置为友元即可。
注意这里实现的几个细节
1. Singleton的构造函数都是protected成员:这是为了保证DesignPattern能够继承并构造他。
2. Sindleton因为需要定义DesignPattern的static对象,所以需要将父类声明为友元,使其能够访问到其构造函数。

  • 26
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值