单列模式详讲

来源:互联网 发布:安卓工具类软件 知乎 编辑:程序博客网 时间:2024/06/13 03:13

一、设计模式简介

(1)什么是设计模式:
设计模式是一套被反复使用,多数人知晓的、经过分类编目的、代码设计的总结,使用设计模式是为了可重用代码,让代码更容易被他人理解,保证代码可靠性。
(2)设计模式的分类:

  • 按照目的可分为三类:

创建型模式,结构型模式,行为型模式;

  • 按照范围,即模式主要处理类之间的关系还是对象之间的关系分为

类模式、对象模式

(3)设计模式主要被广泛应用于面向对象编程;

二、设计模式之单列模式详讲

(1)什么是单列模式
单例模式是一种常用的软件设计模式,也叫单件模式或者单态模式。在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于被外界访问,从而方便对实例个数的控制并节约系统资源如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案

通俗点来说,就是一个类只允许实例化一个对象,那么该对象必然可以在程序的任何地方被访问

(2)为什么要设计单列模式(or单列模式在实际开发中的应用)

最经典也是最容易理解的例子:用windows的都知道,同一时刻只能打开一个任务管理器,不会弹出多个的,为什么呢?因为系统的状态是每时每刻都在变化的,如果你可以同时打开多个任务管理器,而这些管理器显示的内容出现不一致的情况,是不是容易引起用户误解;而如果多个窗口的显示内容一致的话,打开多个窗口是不是很浪费资源;单例模式就是为了解决这种问题而产生的,当前类只允许你拥有一个真实的对象,如果你继续申请,依然返回同一个对象;

(3)如何设计单列类
从上面的单列模式的定义我们知道,单列模式的要求就是设计一个单列类,该类只能实例化一个对象,且该对象可以在一个程序中的任意地方被访问。

那么我们来思考怎么设计这个单列类,假入我们定义一个全局类的对象变量,没错,这个对象确实可以在程序的任何地方被访问,but,这个类还可以在程序的其他地方创建更多的不同的类对象;这样的话,只是符合了单列模式的对象易于被访问的要求,但是没有符合一个类只能有一个实例的要求;

那么怎么实现这个?方法如下:
(1)要保证该类只能实例化一个对象,那么我们必须将能在类外定义该类对象的所有方法设置为私有,这样的话,就不能在类外定义多个对象;所以首先先把类的构造,拷贝构造,赋值运算符重载统统设为私有;

(2)然后在类的内部定义一个单列类的静态指针成员变量,该成员变量就是单列类的唯一实例;该成员变量定义为私有,符合类的封装性;保证类的安全;

(3)类的唯一实例定义完之后,我们还要定义一个在类外获取该唯一实例的类的方法;该方法设置为公有,对外提供一个获取的接口;该方法也是静态;因为只有类的静态方法才能在类外通过 类名::函数名 的方式调用该方法;

假入不将该获取实例的方法定义为静态的,那么我们就无法获取该实例;因为类的非静态方法在类外进行访问,都需要先定义出该类的对象,然后通过该对象访问;但是在类外获取该的实例的时候,该类的对象还没有定义出来;

(4)
单例模式按照设计模式的分类属于创建型模式对象模式;

单例模式的标准定义:单例模式确保某一个类只有一个实例,而且自行实例化并向整个操作系统提供这个实例,这个类成为单例类,它提供全局的访问方法

(5)单列类的设计要求
1>单例类保证全局只有唯一一个自行创建的实例对象。
2.>单例类提供获取这个唯一实例的接口。

三、代码实现详解

(1)饿汉模式– (线程安全,简洁、高效、不用加锁、但是在某些场景下会有缺陷)

方式一: 定义一个单列类的静态指针成员作为单列类的成员变量,这个成员变量就是单列类的唯一实例;

#include<iostream>using namespace std;#include<cassert>class Singleton{public:// 获取唯一对象实例的接口函数   static Singleton* GetInstance()   {      cout<<"GetInstance"<<endl;      assert(_sInstance);      return _sInstance;   } //删除实例对象  static void DelInstance()  {      if (_sInstance)      {          delete _sInstance;          _sInstance=NULL;      }  }  //打印单列类的内容  void Printf()  {      cout<<_data<<endl;  }private:    // 构造函数定义为私有,限制只能在类内创建对象    Singleton()        :_data(1)    {        cout<<"Singleton"<<endl;    }    Singleton(const Singleton& );    Singleton& operator=(const Singleton&);    // 指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例    static Singleton* _sInstance;    // 单例类里面的数据    int _data;};//类的静态成员要在类外进行初始化--静态成员在main函数之前初始化Singleton* Singleton::_sInstance=new Singleton();void TestSingleton(){    //验证在main函数之前单列类对象已经创建---静态成员创建在main之间已经创建好了    cout<<"main"<<endl;    //验证单列类创建的对象为同一个对象,即一个单列类只能实例化出一个对象    Singleton* p1=Singleton::GetInstance();    Singleton* p2=Singleton::GetInstance();    p1->Printf();    p2->Printf();    cout<<(p1==p2)<<endl;    //3.验证在单列类在类外无法实例化出其他对象----私有构造,私有拷贝构造,私有赋值运算符重载的作用    //Singleton s1;//调用构造函数---不行    //Singleton s2(p1);//调用拷贝构造函数---不行    //Singleton s1=p2;//调用赋值运算符重载函数---不行}int main(){    TestSingleton();    return 0;}

方式二:直接在获取唯一实例的成员方法中定义一个局部静态单列类对象;其生命周期随程序,且只在第一次进入函数时初始化一次。

#include<iostream>using namespace std;#include<cassert>class Singleton{public:  // 获取唯一对象实例的接口函数   static Singleton* GetInstance()   {      cout<<"GetInstance"<<endl;      static Singleton sInstance;//静态变量只会创建一次,且生命周期随程序      return &sInstance;   }   //打印单列类的内容  void Printf()  {      cout<<_data<<endl;  }private:    // 构造函数定义为私有,限制只能在类内创建对象    Singleton()        :_data(1)    {        cout<<"Singleton"<<endl;    }    Singleton(const Singleton& );    Singleton& operator=(const Singleton&);    // 单例类里面的数据    int _data;};void TestSingleton(){    Singleton* p1=Singleton::GetInstance();    Singleton* p2=Singleton::GetInstance();    p1->Printf();    p2->Printf();    cout<<(p1==p2)<<endl;}int main(){    TestSingleton();    return 0;}

上面说的是饿汉单例模式,但是这个模式有个不好的地方就是,如果我一直不使用这个对象,而这个对象是已经存在的,它会一直占着那块内存,造成内存浪费;
(2)懒汉模式
上面的饿汉单列出现的不好的地方就是,无论你是否使用单列类对象,他都会先创建一个单列类对象,这样在不使用的情况下就会造成内存浪费;
那么就出现的另一种单列类的写法,就是懒汉模式,这种模式就是说在我们要使用单列类对象的时候才创建单列类,不使用的情况下是不会创建占用内存的单列类对象的;c++中这种思想的体现还有写时拷贝技术;其目的就是保证资源利用的最大化;减少资源的不必要浪费;
1.普通写法

#include<iostream>using namespace std;class Singleton{public:// 获取唯一对象实例的接口函数   static Singleton* GetInstance()   {      cout<<"GetInstance"<<endl;      if (_sInstance==NULL))//只有当第一次进来_inst为空的时候才创建实例      {          _sInstance=new Singleton;      }      return _sInstance;   }   //打印单列类的内容  void Printf()  {      cout<<_data<<endl;  }private:    // 构造函数定义为私有,限制只能在类内创建对象    Singleton()        :_data(1)    {        cout<<"Singleton"<<endl;    }    Singleton(const Singleton& );    Singleton& operator=(const Singleton&);    // 指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例    static Singleton* _sInstance;    // 单例类里面的数据    int _data;};//类的静态成员要在类外进行初始化Singleton* Singleton::_sInstance=NULL;void TestSingleton(){    Singleton* p1=Singleton::GetInstance();    Singleton* p2=Singleton::GetInstance();    p1->Printf();    p2->Printf();    cout<<(p1==p2)<<endl;}int main(){    TestSingleton();    return 0;}

2考虑线程安全的单列类–.普通写法存在的问题就是线程安全问题;
以懒汉单例的普通写法为例,如果此时多线程进行操作,简单点以两个线程为例,假设pthread_1刚判断完 intance 为NULL 为真,准备创建实例的时候,切换到了pthread_2, 此时pthread_2也判断intance为NULL为真,创建了一个实例,再切回pthread_1的时候继续创建一个实例返回,那么此时就不再满足单例模式的要求了, 既然这样,是因为多线程访问出的问题,那我们就来加把锁,使得线程同步;

#include<iostream>using namespace std;#include<mutex>class Singleton{public:// 获取唯一对象实例的接口函数   static Singleton* GetInstance()   {      cout<<"GetInstance"<<endl;      _mtx.lock();//加锁,加锁期间其余线程不能访问该临界区      if (_sInstance==NULL)//只有当第一次进来_inst为空的时候才创建实例      {          _sInstance=new Singleton;      }      _mtx.unlock();//解锁      return _sInstance;//返回实例化的唯一对象   }   //打印单列类的内容  void Printf()  {      cout<<_data<<endl;  }private:    // 构造函数定义为私有,限制只能在类内创建对象    Singleton()        :_data(1)    {        cout<<"Singleton"<<endl;    }    Singleton(const Singleton& );    Singleton& operator=(const Singleton&);    // 指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例    static Singleton* _sInstance;    // 单例类里面的数据    int _data;    static mutex  _mtx;//保证线程安全的互斥锁};//类的静态成员要在类外进行初始化Singleton* Singleton::_sInstance=NULL;mutex Singleton::_mtx;//_mtx会调用mutex默认的无参构造函数,所以不用初始化

3.利用RAII机制自己“造轮子”避免死锁
上述加锁行为确实解决了线程安全问题,但是同时却引入了另一个麻烦,当一个线程进入到if语句创建实例时,可能由于栈空间不够或者某种异常中断导致程序异常终止,但是加的锁并没有进行解锁操作,导致其他线程也无法创建单例对象,从而导致死锁。

RAII:资源获取即初始化,相当于资源分配和初始化时原子操作,其中不会中断

#include<iostream>using namespace std;#include<mutex>class Lock{public:    Lock(mutex& mtx)        :_mtx(mtx)    {        _mtx.lock();    }    ~Lock()    {        _mtx.unlock();    }private:    Lock(const Lock&);//防止拷贝    Lock& operator=(const Lock&);//防止赋值    mutex& _mtx;};class Singleton{public:// 获取唯一对象实例的接口函数   static Singleton* GetInstance()   {      cout<<"GetInstance"<<endl;      lock mtx(_mtx);//加锁--RALL机制      if (_sInstance==NULL)//只有当第一次进来_inst为空的时候才创建实例      {          _sInstance=new Singleton;      }      return _sInstance;//返回实例化的唯一对象   }   //打印单列类的内容  void Printf()  {      cout<<_data<<endl;  }private:    // 构造函数定义为私有,限制只能在类内创建对象    Singleton()        :_data(1)    {        cout<<"Singleton"<<endl;    }    Singleton(const Singleton& );    Singleton& operator=(const Singleton&);    // 指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例    static Singleton* _sInstance;    // 单例类里面的数据    int _data;    static mutex  _mtx;//保证线程安全的互斥锁};//类的静态成员要在类外进行初始化Singleton* Singleton::_sInstance=NULL;mutex Singleton::_mtx;//_mtx会调用mutex默认的无参构造函数,所以不用初始化

3.双检查机制的加锁–高效
前面的加锁确实解决了线程安全的问题,但是我们知道,线程安全只是在第一次创建对象时需要考虑,当对象已经安全的创建以后,其实是不必要在进入临界区判断的;因为如果每次创建对象都要加锁进入临界区,那么这样的话效率会很低;所以在加锁进入临界区之前,再加入一次判断对象是否已经创建,避免后面的程序加锁进入临界区,进行一些不必要的判断和加锁行为,提高程序效率;

// 获取唯一对象实例的接口函数   static Singleton* GetInstance()   {      cout<<"GetInstance"<<endl;      //添加双检查机制,只有创建实例的时候才进行加锁解锁来提高代码效率      if (_sInstance==NULL)      {          lock mtx(_mtx);//加锁--RALL机制          if (_sInstance==NULL)//只有当第一次进来_inst为空的时候才创建实例          {              _sInstance=new Singleton;          }      }      return _sInstance;//返回实例化的唯一对象   } 

4.调用库中的函数:调用库里的函数来避免自己造轮子的开销
c++11的互斥锁:std::lock_guard

// 获取唯一对象实例的接口函数   static Singleton* GetInstance()   {      cout<<"GetInstance"<<endl;      //添加双检查机制,只有创建实例的时候才进行加锁解锁来提高代码效率      if (_sInstance==NULL)      {          lock_guard<mutex> lock(_mtx);//          if (_sInstance==NULL)//只有当第一次进来_inst为空的时候才创建实例          {              _sInstance=new Singleton;          }      }      return _sInstance;//返回实例化的唯一对象   } 

5.实例销毁处理
代码写到这里,基本已经差不多了,但是有人会想既然实例是new出来的,那么我们为什么不用delete将其释放掉呢,难道不会内存泄漏么?结果是当然会的,为什么不析构单例的实例呢,是因为它本身是一个静态的全局实例,之所以只能定义唯一一个,肯定是用的次数比较多,如果刚释放完空间又来创建单例对象,肯定就不得不偿失了。反过来说,单例模式一共就一个实例,考虑其本质特性,不释放的话在单例模式下可能会更好一些,待系统使用完毕后自动回收资源也是一个不错的选择。

在实际项目中,特别是客户端开发,其实是不在乎这个实例的销毁的。因为,全局就这么一个变量,全局都要用,它的生命周期伴随着软件的生命周期,软件结束了,它也就自然而然的结束了,因为一个程序关闭之后,它会释放它占用的内存资源的,所以,也就没有所谓的内存泄漏了。但是,有以下情况,是必须需要进行实例销毁的:
1>在类中,有一些文件锁,文件句柄,数据库连接等等,这些随着程序的关闭而不会立即关闭的资源,必须要在程序关闭前,进行手动释放;
2>具有强迫症的程序员。

#include<iostream>using namespace std;#include<mutex>class Singleton{public:// 获取唯一对象实例的接口函数   static Singleton* GetInstance()   {      cout<<"GetInstance"<<endl;      //添加双检查机制,只有创建实例的时候才进行加锁解锁来提高代码效率      if (_sInstance==NULL)      {          lock_guard<mutex>lock(_mtx);          if (_sInstance==NULL)//只有当第一次进来_inst为空的时候才创建实例          {              _sInstance=new Singleton;          }      }      return _sInstance;//返回实例化的唯一对象   }   static void DelInstanc()//在某些情况下才需要手动释放   {       lock_guard<mutex> lock(_mtx);//防止对象被释放多次       if (_sInstance)       {           cout << "delete" << endl;           delete _sInstance;           _sInstance = NULL;//防止野指针的出现       }   }  //打印单列类的内容  void Printf()  {      cout<<_data<<endl;  }private:    // 构造函数定义为私有,限制只能在类内创建对象    Singleton()        :_data(1)    {        cout<<"Singleton"<<endl;    }    Singleton(const Singleton& );    Singleton& operator=(const Singleton&);    ~Singleton()    {        //关闭文件锁,文件句柄,数据库连接等    }    // 指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例    static Singleton* _sInstance;    // 单例类里面的数据    int _data;    static mutex  _mtx;//保证线程安全的互斥锁};//类的静态成员要在类外进行初始化Singleton* Singleton::_sInstance=NULL;mutex Singleton::_mtx;//_mtx会调用mutex默认的无参构造函数,所以不用初始化

6.保证自动正确的释放
在上面的代码中确实可以手动的正确释放单列类对象;但是有时候我们会忘记释放;
这时采用两个办法可以办证正确的自动释放单列类对象;
思来想去,我们知道main函数是所有线程的主线程,在它调用完成之后其他线程绝对早已退出那么我们如果在主线程调用结束之后在对单例对象进行释放,那么绝逼是不会有任何问题的,接下来就是该想想如何实现了。

1>atexit,注册回调函数–在程序结束时,回调函数自动被调用,释放单列类类对象;

#include<iostream>using namespace std;#include<mutex>class Singleton{public:// 获取唯一对象实例的接口函数   static Singleton* GetInstance()   {      cout<<"GetInstance"<<endl;      //添加双检查机制,只有创建实例的时候才进行加锁解锁来提高代码效率      if (_sInstance==NULL)      {          lock_guard<mutex>lock(_mtx);          if (_sInstance==NULL)//只有当第一次进来_inst为空的时候才创建实例          {              _sInstance=new Singleton;          }      }      return _sInstance;//返回实例化的唯一对象   }   static void DelInstanc()//在某些情况下才需要手动释放   {       lock_guard<mutex> lock(_mtx);//防止对象被释放多次       if (_sInstance)       {           cout << "delete" << endl;           delete _sInstance;           _sInstance = NULL;//防止野指针的出现       }   }  //打印单列类的内容  void Printf()  {      cout<<_data<<endl;  }private:    // 构造函数定义为私有,限制只能在类内创建对象    Singleton()        :_data(1)    {        cout<<"Singleton"<<endl;    }    Singleton(const Singleton& );    Singleton& operator=(const Singleton&);    ~Singleton()    {        //关闭文件锁,文件句柄,数据库连接等    }    // 指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例    static Singleton* _sInstance;    // 单例类里面的数据    int _data;    static mutex  _mtx;//保证线程安全的互斥锁};//类的静态成员要在类外进行初始化Singleton* Singleton::_sInstance=NULL;mutex Singleton::_mtx;//_mtx会调用mutex默认的无参构造函数,所以不用初始化int main(){    Singleton::GetInstance()->Print();    Singleton::GetInstance()->Print();    atexit(Singleton::DelInstanc);//注册回调函数,在main函数之后调用析构}

2>类中类–在单列类中定义一个类,此类的析构函数内部调用单列类的释放单列对象的函数;定义一个全局的该类对象;那么次对象的声明周期和我们期望的单列类的对象的声明周期一直,都是随程序的结束而被释放;那么在该类对象被释放时调用自己的析构函数,从而将单列类的对象正确的释放;

class Singleton{public:// 获取唯一对象实例的接口函数   static Singleton* GetInstance()   {      cout<<"GetInstance"<<endl;      //添加双检查机制,只有创建实例的时候才进行加锁解锁来提高代码效率      if (_sInstance==NULL)      {          lock_guard<mutex>lock(_mtx);          if (_sInstance==NULL)//只有当第一次进来_inst为空的时候才创建实例          {              _sInstance=new Singleton;          }      }      return _sInstance;//返回实例化的唯一对象   }   static void DelInstanc()//在某些情况下才需要手动释放   {       lock_guard<mutex> lock(_mtx);//防止对象被释放多次       if (_sInstance)       {           cout << "delete" << endl;           delete _sInstance;           _sInstance = NULL;//防止野指针的出现       }   }  //打印单列类的内容  void Printf()  {      cout<<_data<<endl;  }  struct GC//实现方式二:类中类  {      ~GC()      {          DelInstanc();//调用析构函数      }  };private:    // 构造函数定义为私有,限制只能在类内创建对象    Singleton()        :_data(1)    {        cout<<"Singleton"<<endl;    }    Singleton(const Singleton& );    Singleton& operator=(const Singleton&);    ~Singleton()    {        //关闭文件锁,文件句柄,数据库连接等    }    // 指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例    static Singleton* _sInstance;    // 单例类里面的数据    int _data;    static mutex  _mtx;//保证线程安全的互斥锁};//类的静态成员要在类外进行初始化Singleton* Singleton::_sInstance=NULL;mutex Singleton::_mtx;//_mtx会调用mutex默认的无参构造函数,所以不用初始化int main(){    Singleton::GetInstance()->Print();    Singleton::GetInstance()->Print();}Singleton::GC gc;

7.利用内存珊栏优化

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

四、总结

懒汉模式:只有当调用对象是才创建对象–相对而言,复杂—-但适用性高,各种场景下都适用。

饿汉模式:在函数一开始就创建(main)–简单高效,但是在某些场景下有缺陷—-适用性会受到限制,动态库。

懒汉模式和饿汉模式唯一的不同就是,这个唯一的实例创建的时机不同;饿汉模式不存在线程安全问题,而懒汉模式存在线程安全问题,必须加锁,加锁的时候,又存在死锁的问题,所以就需要用RAII机制解决死锁问题,解缺方法1是自己造轮子,方法二是调用c++11中的互斥锁;加锁和死锁的问题解决之后,又出现加锁的效率问题,所以提出了双检查机制,避免每次加锁解锁的时间开销;在开辟空间获取唯一实例的时候,由于编译器可能会对该句程序的指令进行优化和乱序执行,在高并发情况下可能会出现将一个没有初始化的空间赋值,可能出现崩溃,所以选择用内存珊栏作为优化;

最后再说说单列类对象的销毁;一般情况下不用手动的去销毁对象;因为单列类对象的生命周期随程序就完全符合实际开发的需求;当程序结束后,单列类对象就会调用自己的析构函数自动销毁;但是有时候只调用默认析构函数将单列类对象销毁远远不够,类中的文件句柄,数据库链接,网络socket等随着对象的销毁不会立刻关闭;所以需要手动的来正确销毁对象;
当我们写出正确的销毁对象的函数之后,那我们就要在类外手动的调用这个函数,但是就像c++中的智能指针为了防止忘记释放造成的内存浪费;这时,我们也可以用两种办法实现单列类的RAII自动销毁技术,方法 一就是注册回调函数,方法二就是实现类中类;

五、单列类的优缺点以及使用场景

(1)单例模式的优点
1.提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它;
2.由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
3.允许可变数目的实例,基于单例模式我们可以扩展,使用与单例控制相似的方法来获得指定个数的对象实例。(即单例类内有多个静态对象指针成员,每次当单例类被引用时随机分配一个实例对象);

(2)单例模式的缺点
1.因为单例模式没有抽象层,所以单例类的扩展有很大的困难
2.单例类的职责过重,即是工厂角色,提供了工厂的方法,同时又充当了产品的角色;
3.滥用单例类会带来一些负面问题

(3)单例模式的适用场景
1.系统只需要一个实例对象,或者考虑到资源消耗的太大而只允许创建一个对象。

2.客户调用类的单个实例只允许使用一个公共访问点,除了该访问点之外不允许通过其它方式访问该实例.(就是共有的静态方法);