设计模式——单例模式

设计模式:

设计模式代表了最佳实践,是软件开发过程中面临一般问题的解决方案。 设计模式是一套被反复使用、经过分类、代码设计总结的经验。

单例模式

单例模式也叫单件模式。Singleton是一个非常常用的设计模式,几乎所有稍微大一些的程序都会使用到它,所以构建一个线程安全并且 高效的Singleton很重要。

1. 单例类保证全局只有一个唯一实例对象。

2. 单例类提供获取这个唯一实例的接口。

由于要求只生成一个实例,因此我们必须把构造函数的访问权限标记为protected或private,限制只能在类内创建对象.

单例类要提供一个访问唯一实例的接口函数(全局访问点),就需要在类中定义一个static函数,返回在类内部唯一构造的实例。

(这样还可以确保直接用类名就能访问到该唯一实例,不必用到实例化出的对象名去调用)


 两个概念:

 懒汉模式 (lazy loading ):第一次调用GetInstance才创建实例对象,比较复杂
 饿汉模式:  程序一运行,就创建实例对象、简洁高效 ,但有些场景下不适用 

方法一:不考虑线程安全,只适用于单线程环境的单例类

定义一个静态的实例,在需要的时候创建该实例 (懒汉模式)

class Singleton
{
public:
	//获取唯一对象实例的接口函数
	static Singleton* GetInstance()
	{
		if (_instance == NULL)
		{
			_instance = new Singleton();
		}
		return _instance;
	}
	static void DelInstance()
	{
		if (_instance != NULL)
		{
			delete _instance;
			_instance = NULL;
		}
	}
	void Print()
	{
		cout << _data << endl;
	}
protected:
	//构造函数标记为protected或private,限制只能在类内创建对象
	Singleton()
		:_data(5)
	{}

	//防拷贝
	Singleton(const Singleton&);
	Singleton operator=(const Singleton&);
private:		
	//指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例
	static Singleton* _instance;	  // 单实例对象
	int _data;  //单实例对象中的数据
};
// 静态成员在类外初始化
Singleton* Singleton::_instance = NULL;

  这种方法是最简单、最普遍的方法。只有在_instance为NULL的时候才会创建一个实例以避免重复创建。同时我们把构造函数定义为私有函数,这样就能确保只创建一个实例。

但是上述的代码在单线程的时候工作正常,在多线程的情况下就有问题了。

  设想如果两个线程同时运行到判断_instance是否为NULL的 if 语句那里,并且_instance之前并未创建时,这两个线程各自就都会创建一实例,这是就无法满足单例模式的要求了。


 方法二:能在多线程环境下工作,但是效率不高

为了保障在多线程环境下只得到一个实例,需要加一把互斥锁。把上述代码稍作修改,即:

ps: 下面部分的加锁使用了C++11库的互斥锁

class Singleton
{
public:
	//获取唯一对象实例的接口函数
	static Singleton* GetInstance()
	{
		//lock();        //C++中没有直接的lock()
		//RAII
		//lock lk;
		_sMtx.lock();   //C++11
		if (_instance == NULL)
		{
			_instance = new Singleton();
		}
		//unlock();
		_sMtx.unlock();
		return _instance;
	}
	static void DelInstance()
	{
		if (_instance != NULL)
		{
			delete _instance;
			_instance = NULL;
		}
	}
	void Print()
	{
		cout << _data << endl;
	}
protected:
	//构造函数标记为protected或private,限制只能在类内创建对象
	Singleton()
		:_data(5)
	{}

	//防拷贝
	Singleton(const Singleton&);
	Singleton operator=(const Singleton&);

private:
	//指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例
	static Singleton* _instance;	  // 单实例对象
	int _data;								  // 单实例对象中的数据
	static mutex _sMtx;	              // 互斥锁
};
// 静态成员在类外初始化
Singleton* Singleton::_instance = NULL;
mutex Singleton::_sMtx;

  设想有两个线程同时想创建一个实例,由于在一个时刻,只有一个线程能得到互斥锁,所以当第一个线程加上锁后,第二个线程就只能等待。当第一个线程发现实例还没有创建时,它就建立一个实例。接着第一个线程释放锁,此时第二个线程进入并上锁,这个时候由于实例已经被第一个线程创建出来了,第二个线程就不会重复创建实例了,这样就保证在多线程环境下只能得到一个实例。

  但是,每次获取唯一实例,程序都会加锁,而加锁是一个非常耗时的操作,在没有必要的时候,我们要尽量避免,否则会影响性能。


 方法三:使用双重检查,提高效率,避免高并发场景下每次获取实例对象都进行加锁,并使用内存栅栏防止重排

class Singleton
{
public:
	//获取唯一对象实例的接口函数
	static Singleton* GetInstance()
	{
		// 使用双重检查,提高效率,避免高并发场景下每次获取实例对象都进行加锁
		if (_instance == NULL)
		{
			std::lock_guard<std::mutex> lck(_sMtx);
			if (_instance == NULL)
			{
				// tmp = new Singleton()分为以下三个部分
				// 1.分配空间2.调用构造函数3.赋值
				// 编译器编译优化可能会把2和3进行指令重排,这样可能会导致高并发场景下,其他线程获取到未调用构造函数初始化的对象
				// 以下加入内存栅栏进行处理,防止编译器重排栅栏后面的赋值到内存栅栏之前
				Singleton* tmp = new Singleton();
				MemoryBarrier(); //内存栅栏
				_instance = tmp;
			}
		}
		return _instance;
	}
	static void DelInstance()
	{
		if (_instance != NULL)
		{
			delete _instance;
			_instance = NULL;
		}
	}
	void Print()
	{
		cout << _data << endl;
	}
protected:
	//构造函数标记为protected或private,限制只能在类内创建对象
	Singleton()
		:_data(5)
	{}

	//防拷贝
	Singleton(const Singleton&);
	Singleton operator=(const Singleton&);

private:
	//指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例
	static Singleton* _instance;	  // 单实例对象
	int _data;								  // 单实例对象中的数据
	static mutex _sMtx;	              // 互斥锁
};
// 静态成员在类外初始化
Singleton* Singleton::_instance = NULL;
mutex Singleton::_sMtx;

  试想,当实例还未创建时,由于 Singleton == NULL ,所以很明显,两个线程都可以通过第一重的 if 判断 ,进入第一重 if 语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 if 判断 ,而另外的一个线程则会在 lock 语句的外面等待。而当第一个线程执行完 new  Singleton()语句退出锁定区域,第二个线程便可以进入 lock 语句块,此时,如果没有第二重Singleton == NULL的话,那么第二个线程还是可以调用 new  Singleton()语句,第二个线程仍旧会创建一个 Singleton 实例,这样也还是违背了单例模式的初衷的,所以这里必须要使用双重检查锁定(第二层if 判断必须存在)。

   多数现代计算机为了提高性能而采取乱序执行,这使得内存栅栏成为必须。barrier就象是代码中的一个栅栏,将代码逻辑分成两段,barrier之前的代码和barrier之后的代码在经过编译器编译后顺序不能乱掉。也就是说,barrier之后的代码对应的汇编,不能跑到barrier之前去,反之亦然。之所以这么做是因为在我们这个场景中,如果编译器为了榨取CPU的performace而对汇编指令进行重排,其它线程获取到未调用构造函数初始化的对象,很有可能导致出错。

   只有第一次调用_instance为NULL,并且试图创建实例的时候才需要加锁,当_instance已经创建出来后,则没必要加锁。这样的修改比之前的时间效率要好很多。

但是这样的实现比较复杂,容易出错,我们还可以利用饿汉模式,创建相对简洁高效的单例模式。


方法四:饿汉模式--简洁、高效、不用加锁、但是在某些场景下会有缺陷

  因为静态成员的初始化在程序开始时,也就是进入主函数之前,由主线程以单线程方式完成了初始化,所以静态初始化实例保证了线程安全性。在性能要求比较高时,就可以使用这种方式,从而避免频繁的加锁和解锁造成的资源浪费。

class Singleton
{
public:
	//获取唯一对象实例的接口函数
	static Singleton* GetInstance()
	{
		assert(_instance);
		return _instance;
	}
	void Print()
	{
		cout << _data << endl;
	}
protected:
	//构造函数标记为protected或private,限制只能在类内创建对象
	Singleton()
		:_data(5)
	{}

	//防拷贝
	Singleton(const Singleton&);
	Singleton operator=(const Singleton&);

private:
	static Singleton* _instance;	  // 单实例对象
	int _data;			 // 单实例对象中的数据
};
Singleton* Singleton::_instance = new Singleton;

 代码实现非常简洁。创建的实例_instance并不是在第一次调用GetInstance接口函数时才创建,而是在初始化静态变量的时候就创建一个实例。如果按照该方法会过早的创建实例,从而降低内存的使用效率。 

方法五:方法四还可以再简化点

class Singleton
{
public:
	//获取唯一对象实例的接口函数
	static Singleton* GetInstance()
	{
		static Singleton instance;
		return &instance;
	}
	void Print()
	{
		cout << _data << endl;
	}
protected:
	//构造函数标记为protected或private,限制只能在类内创建对象
	Singleton()
		:_data(5)
	{}

	//防拷贝
	Singleton(const Singleton&);
	Singleton operator=(const Singleton&);

private:
	int _data;	 // 单实例对象中的数据
};

 实例销毁

 此处使用了一个内部GC类,而该类的作用就是用来释放资源

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//带RAII GC自动回收实例对象的方式
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

class Singleton
{
public:
	// 获取唯一对象实例的接口函数
	static Singleton* GetInstance()
	{
		assert(_instance);
		return _instance;
	}
	// 删除实例对象
	static void DelInstance()
	{
		if (_instance)
		{
			delete _instance;
			_instance = NULL;
		}
	}
	void Print()
	{
		cout << _data << endl;
	}
	class GC
	{
	public:
		~GC()
		{
			cout << "DelInstance()" << endl;
			DelInstance();
		}
	};
private:
	Singleton()
		:_data(5)
	{}
	static Singleton*_instance;
	int _data;
};
// 静态对象在main函数之前初始化,这时只有主线程运行,所以是线程安全的。
Singleton* Singleton::_instance = new Singleton;
// 使用RAII,定义全局的GC对象释放对象实例
Singleton::GC gc;

    在程序运行结束时,系统会调用Singleton中GC的析构函数,该析构函数会进行资源的释放。

 

posted @ 2016-07-23 23:16  ProLyn  阅读(762)  评论(2编辑  收藏  举报