单例模式

什么是单例模式

单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个实例。即一个类只有一个对象实例。

单例模式的实现

实现单例模式一般有两种方式:饿汉模式和懒汉模式。

饿汉模式

我们用一个洗碗的例子来解释饿汉模式和懒汉模式。
现在有饿汉和一个脏了的碗,饿汉希望下次想吃饭的时候能直接盛饭吃,所以他就先把碗先洗好,这样随时都有干净的碗可以用。

同样,我们在程序运行之初就将对象创建好,就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。这样程序运行中想要调用这个实例时都可以直接使用。


结合上面的思想,我们想在程序运行之初就要设置好这个实例,很自然的,我们就想到了静态变量。再将构造函数/拷贝构造函数/赋值运算符重载统统禁掉(设置成私有,类外就无法调用了),就能保证实例不会通过别的方式创建。

然后我们再定义一个函数,这个函数返回单例的地址,就能保证我们能操作同一个单例了。(注意,这个函数必须设置成静态的,倘若不设置成静态的,那么我们只有创建了对象才能调他,但是我们的初衷又是不要通过别的方式创建对象,这就成了先有鸡还是先有蛋的问题了)。

class Hungry
{
public:
	static Hungry* GetInstance()//对外提供一个静态的成员函数,返回我们的单例
	{
		return &onlyInstance;
	}

private:
	static Hungry onlyInstance;//定义一个静态的实例

private://将其他可能产生实例的函数都弄成私有,设置为删除函数是C++11的新写法
	Hungry() {};
	Hungry(const Hungry&) = delete;
	Hungry& operator=(const Hungry&) = delete;
	
};

Hungry Hungry::onlyInstance;//单例别忘了类外初始化

int main()
{
	Hungry* a = Hungry::GetInstance();
}

饿汉模式的优点:

  1. 简单
  2. 因为我们在程序启动时就设置好了单例,所以饿汉模式是线程安全的。

饿汉模式的缺点:

  1. 会拖慢程序启动的速度。
  2. 如果有多个单例类对象实例启动顺序不确定。

懒汉模式

懒汉和饿汉不一样,他看了一眼脏的碗,内心没有丝毫波动,根本不想洗碗。等他饿了的时候,才慢吞吞的去把碗洗了,才盛饭。

那对应到程序中,只有我们首次想要使用单例的时候,才去创建单例,这就是懒汉模式。

如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等
等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好.


#include<iostream>
#include<mutex>
#include<thread>
using namespace std;
class Lazy
{
public:

	class gc//内嵌垃圾回收类
	{
	public:
		~gc()
		{
			if (Lazy::OnlyInstance)
				delete Lazy::OnlyInstance;
		}
	};

	static gc g;//定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象

	static Lazy* GetInstance()//返回单例
	{
		if (OnlyInstance == nullptr)
		{
			m_mutex.lock();
			if (OnlyInstance == nullptr)//?为什么要双层检查
			{
				OnlyInstance = new Lazy();
			}
			m_mutex.unlock();
		}
		return OnlyInstance;
	}

private:
	static Lazy* OnlyInstance;//单例指针
	static mutex m_mutex;//互斥量
private:
	Lazy() {};
	Lazy(const Lazy&) {};
	Lazy& operator=(const Lazy&) {};
};

Lazy* Lazy::OnlyInstance = nullptr;//初始化
mutex Lazy::m_mutex;
Lazy::gc g;



在懒汉模式中,我们用一个静态的指针变量来指向我们的单例,静态指针的初始化让我们在程序启动上没有太大的压力。定义一个GetInstance接口,返回单例指针。

要注意,因为懒汉模式的单例是有需要才去创建的,所以在首次创建实例要考虑到线程安全的问题。

	static Lazy* GetInstance()//返回单例
	{
		if (OnlyInstance == nullptr)         //1                  
		{
			m_mutex.lock();
			if (OnlyInstance == nullptr)     //2
			{
				OnlyInstance = new Lazy();
			}
			m_mutex.unlock();
		}
		return OnlyInstance;
	}

当两个线程并行执行的时候,可能他们都判断实例未被创建,所以实例就会创建多个。就不满足单例模式的特性了。

要想让懒汉模式变得线程安全,那只要将对公共资源的操作原子化就可以了。而我们熟悉的方式就是加锁。加锁的步骤很简单,这里只说双重检查的原因

我们看上面的代码,发现在1,2两处都加了检查机制,为什么要两层检查呢?

首先说第一层检查:

当一个线程运行到第一层检查,如果发现实例已经创建,就直接返回。
如果没有创建则通过第一层检查。
进入第一层检查后,我们让各个线程竞争锁,竞争到锁的线程(我们用A表示)来进行第二层检查

第二层检查:
当线程A获取锁后,再次判断,如果实例仍未被创建,则创建实例。如果实例已经被创建,那么就是上次拿到锁的线程(B)创建了实例(B创建的时候A一直阻塞在获得锁的地方,A并不知道B先于他获得了锁,如果不再次判断,那么仍然有可能创建多个实例)。返回即可。

懒汉模式的优点:

  1. 第一次要使用单例的时候才去创建, 进程启动的时候无负载。
  2. 多个单例启动顺序可以自由控制多个单例启动顺序可以自由控制

缺点:

  1. 代码复杂
  2. 需要考虑线程安全,由用户加锁
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章