zoukankan      html  css  js  c++  java
  • 设计模式——单例模式

    设计模式:

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

    单例模式

    单例模式也叫单件模式。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的析构函数,该析构函数会进行资源的释放。

  • 相关阅读:
    github for window的代理设置方法
    深入理解ANGULARUI路由_UIROUTER
    HTML:document.activeElement
    Ubuntu 安装java环境搭建
    svn 服务器搭建
    nginx tomcat 动静分离
    mysql5.5 修改字符集
    ansible 安装
    基于apache的tomcat负载均衡和集群配置
    数据库值错误怎么改?
  • 原文地址:https://www.cnblogs.com/Lynn-Zhang/p/5699819.html
Copyright © 2011-2022 走看看