• 单例模式


      单例模式也称为单件模式、单子模式,可能是使用最广泛的设计模式。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。有很多地方需要这样的功能模块,如系统的日志输出,GUI应用必须是单鼠标,MODEM的联接需要一条且只需要一条电话线,操作系统只能有一个窗口管理器,一台PC连一个键盘。

           单例模式有许多种实现方法,在C++中,甚至可以直接用一个全局变量做到这一点,但这样的代码显的很不优雅。 使用全局对象能够保证方便地访问实例,但是不能保证只声明一个对象——也就是说除了一个全局实例外,仍然能创建相同类的本地实例。

     

    有的时候,总是容易把全局变量和单例模式给弄混了,下面就剖析一下全局变量和单例模式相比的缺点

      首先,全局变量呢就是对一个对象的静态引用,全局变量确实可以提供单例模式实现的全局访问这个功能,但是,它并不能保证您的应用程序中只有一个实例,同时,在编码规范中,也明确指出,应该要少用全局变量,因为过多的使用全局变量,会造成代码难读,还有就是全局变量并不能实现继承(虽然单例模式在继承上也不能很好的处理,但是还是可以实现继承的)而单例模式的话,其在类中保存了它的唯一实例,这个类,它可以保证只能创建一个实例,同时,它还提供了一个访问该唯一实例的全局访问点。

      《设计模式》一书中给出了一种很不错的实现: 定义一个单例类,使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例
           单例模式通过类本身来管理其唯一实例,这种特性提供了解决问题的方法。唯一的实例是类的一个普通对象,但设计这个类时,让它只能创建一个实例并提供对此实例的全局访问。唯一实例类Singleton在静态成员函数中隐藏创建实例的操作。习惯上把这个成员函数叫做Instance(),它的返回值是唯一实例的指针。

    定义如下:

    class CSingleton 
    { 
    private: 
        CSingleton(){}   //构造函数是私有的 
        static CSingleton *m_pInstance; 
    public: 
        static CSingleton * GetInstance() 
        { 
            if(m_pInstance == NULL)  //判断是否第一次调用 
                m_pInstance = new CSingleton(); 
            return m_pInstance; 
        } 
    };

      用户访问唯一实例的方法只有GetInstance()成员函数。如果不通过这个函数,任何创建实例的尝试都将失败,因为类的构造函数是私有的。GetInstance()使用懒惰初始化,也就是说它的返回值是当这个函数首次被访问时被创建的。这是一种防弹设计——所有GetInstance()之后的调用都返回相同实例的指针:

    CSingleton* p1 = CSingleton :: GetInstance();
    CSingleton* p2 = p1->GetInstance();
    CSingleton & ref = * CSingleton :: GetInstance();

      对GetInstance稍加修改,这个设计模板便可以适用于可变多实例情况,如一个类允许最多五个实例。


    单例类CSingleton有以下特征:
      1. 它有一个指向唯一实例的静态指针m_pInstance,并且是私有的;
      2. 它有一个公有的函数,可以获取这个唯一的实例,并且在需要的时候创建该实例;
      3. 它的构造函数是私有的,这样就不能从别处创建该类的实例。
      大多数时候,这样的实现都不会出现问题。有经验的读者可能会问,m_pInstance指向的空间什么时候释放呢?更严重的问题是,该实例的析构函数什么时候执行?如果在类的析构行为中有必须的操作,比如关闭文件,释放外部资源,那么上面的代码无法实现这个要求。我们需要一种方法,正常的删除该实例。可以在程序结束时调用GetInstance(),并对返回的指针掉用delete操作。这样做可以实现功能,但不仅很丑陋,而且容易出错。因为这样的附加代码很容易被忘记,而且也很难保证在delete之后,没有代码再调用GetInstance函数。
      一个妥善的方法是让这个类自己知道在合适的时候把自己删除,或者说把删除自己的操作挂在操作系统中的某个合适的点上,使其在恰当的时候被自动执行。我们知道,程序在结束的时候,系统会自动析构所有的全局变量。事实上,系统也会析构所有的类的静态成员变量,就像这些静态成员也是全局变量一样。利用这个特征,我们可以在单例类中定义一个这样的静态成员变量,而它的唯一工作就是在析构函数中删除单例类的实例。如下面的代码中的CGarbo类(Garbo意为垃圾工人):

    class CSingleton 
    { 
    private: 
        CSingleton(){} 
        static CSingleton *m_pInstance; 
        class CGarbo   //它的唯一工作就是在析构函数中删除CSingleton的实例 
        { 
        public: 
            ~CGarbo() 
                { 
                    if(CSingleton::m_pInstance) 
                        delete CSingleton::m_pInstance; 
                } 
        }; 
        static CGarbo Garbo;  //定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数 
    public: 
        static CSingleton * GetInstance() 
        { 
            if(m_pInstance == NULL)  //判断是否第一次调用 
                m_pInstance = new CSingleton(); 
            return m_pInstance; 
        } 
    }; 

    类CGarbo被定义为CSingleton的私有内嵌类,以防该类被在其他地方滥用。

    程序运行结束时,系统会调用CSingleton的静态成员Garbo的析构函数,该析构函数会删除单例的唯一实例。使用这种方法释放单例对象有以下特征:
      1. 在单例类内部定义专有的嵌套类;
      2. 在单例类内定义私有的专门用于释放的静态成员;
      3. 利用程序在结束时析构全局变量的特性,选择最终的释放时机;
      4. 使用单例的代码不需要任何操作,不必关心对象的释放。

     

    但是添加一个类的静态对象,总是让人不太满意,所以有人用如下方法来重新实现单例和解决它相应的问题,代码如下:

    class CSingleton 
    { 
    private: 
        CSingleton(){}  //构造函数是私有的 
    public: 
        static CSingleton & GetInstance() 
        { 
            static CSingleton instance;   //局部静态变量 
            return instance; 
        } 
    }; 

      使用局部静态变量,非常强大的方法,完全实现了单例的特性,而且代码量更少,也不用担心单例销毁的问题。(局部静态变量的链接性请参见《C++ Primer Plus》变量存储持续性、作用域和链接性章节)

    但使用此种方法也会出现问题,当如下方法使用单例时问题来了,

    Singleton singleton = Singleton :: GetInstance();

      这么做就出现了一个类拷贝的问题,这就违背了单例的特性。产生这个问题原因在于:编译器会为类生成一个默认的拷贝构造函数,来支持类的拷贝。最后没有办法,我们要禁止类拷贝和类赋值,禁止程序员用这种方式来使用单例,当时领导的意思是GetInstance()函数返回一个指针而不是返回一个引用,函数的代码改为如下:

    class CSingleton 
    { 
    private: 
        CSingleton(){}  //构造函数是私有的 
    public: 
        static CSingleton * GetInstance() 
        { 
            static CSingleton instance;   //局部静态变量 
            return &instance; 
        } 
    };

    但我总觉的不好,为什么不让编译器不这么干呢。这时我才想起可以显示的声明类的拷贝构造函数,和重载 = 操作符,新的单例类如下:

    class CSingleton 
    { 
    private: 
        CSingleton(){}    //构造函数是私有的 
        CSingleton(const CSingleton &); 
        CSingleton & operator = (const CSingleton &); 
    public: 
        static CSingleton & GetInstance() 
        { 
            static CSingleton instance;   //局部静态变量 
            return instance; 
        } 
    }; 

      关于Singleton(const Singleton);和 Singleton & operate = (const Singleton&);函数,需要声明成私有的,并且只声明不实现。这样,如果用上面的方式(Singleton singleton = Singleton :: GetInstance();)来使用单例时,不管是在友元类中还是其他的,编译器都是报错。不知道这样的单例类是否还会有问题,但在程序中这样子使用已经基本没有问题了。

    考虑到线程安全、异常安全,可以做以下扩展

    class Lock 
    { 
    private:        
        CCriticalSection m_cs; 
    public: 
        Lock(CCriticalSection  cs) : m_cs(cs) 
        { 
            m_cs.Lock(); 
        } 
        ~Lock() 
        { 
            m_cs.Unlock(); 
        } 
    }; 
    
    class Singleton 
    { 
    private: 
        Singleton(); 
        Singleton(const Singleton &); 
        Singleton& operator = (const Singleton &); 
    public: 
        static Singleton *Instantialize(); 
        static Singleton *pInstance; 
        static CCriticalSection cs; 
    }; 
    
    Singleton* Singleton::pInstance = 0; 
    
    Singleton* Singleton::Instantialize() 
    { 
        if(pInstance == NULL) 
        {   //double check 
            Lock lock(cs);           //用lock实现线程安全,用资源管理类,实现异常安全 
            //使用资源管理类,在抛出异常的时候,资源管理类对象会被析构,析构总是发生的无论是因为异常抛出还是语句块结束。 
            if(pInstance == NULL) 
            { 
                pInstance = new Singleton(); 
            } 
        } 
        return pInstance; 
    } 

      之所以在Instantialize函数里面对pInstance 是否为空做了两次判断,因为该方法调用一次就产生了对象,pInstance == NULL 大部分情况下都为false,如果按照原来的方法,每次获取实例都需要加锁,效率太低。而改进的方法只需要在第一次 调用的时候加锁,可大大提高效率。

    【转自】http://blog.csdn.net/hackbuteer1/article/details/7460019

    补充:

      如果在一开始调用 GetInstance()时,是由两个线程同时调用的(这种情况是很常见的),注意是同时,(或者是一个线程进入 if 判断语句后但还没有实例化 Singleton 时,第二个线程到达,此时 singleton 还是为 null), 这样的话,两个线程均会进入 GetInstance(),而后由于是第一次调用 GetInstance(), 所以存储在 Singleton 中的静态变量 singleton 为 null ,这样的话,就会让两个线程均通过 if 语句的条件判断, 然后调用 new Singleton()了.

    public static Singleton GetInstance() 
            {  
                if (singleton == null) 
                { 
                    singleton = new Singleton(); 
                } 
                return singleton; 
            } 

      这样的话,问题就出来了,因为有两个线程,所以会创建两个实例, 很显然,这便违法了单例模式的初衷了, 那么如何解决上面出现的这个问题(即多线程下使用单例模式时有可能会创建多个实例这一现象)呢?

      其实,这个是很好解决的, 您可以这样思考这个问题: 由于上面出现的问题中涉及到多个线程同时访问这个 GetInstance(), 那么您可以先将一个线程锁定,然后等这个线程完成以后,再让其他的线程访问 GetInstance()中的 if 段语句, 比如,有两个线程同时到达 如果 singleton != null 的话,那么上面提到的问题是不会存在的,因为已经存在这个实例了,这样的话, 所有的线程都无法进入 if 语句块, 也就是所有的线程都无法调用语句 new Singleton()了, 这样还是可以保证应用程序生命周期中的实例只存在一个, 但是如果此时的 singleton == null 的话, 那么意味着这两个线程都是可以进入这个 if 语句块的,那么就有可能出现上面出现的单例模式中有多个实例的问题,此时,我可以让一个线程先进入 if 语句块,然后我在外面对这个 if 语句块加锁,对第二个线程呢,由于 if 语句进行了加锁处理,所以这个进程就无法进入 if 语句块而处于阻塞状态,当进入了 if 语句块的线程完成 new  Singleton()后,这个线程便会退出 if 语句块,此时,第二个线程就从阻塞状态中恢复,即就可以访问 if 语句块了,但是由于前面的那个线程已近创建了 Singleton 的实例,所以 singleton != null ,此时,第二个线程便无法通过 if 语句的判断条件了,即无法进入 if 语句块了,这样便保证了整个生命周期中只存在一个实例,也就是只有第一个线程创建了 Singleton 实例,第二个线程则无法创建实例。

     下面就来重新改进前面 Demo 中的 Singleton 类,使其在多线程的环境下也可以实现单例模式的功能

    namespace Singleton 
    { 
        public class Singleton 
        { 
            //定义一个私有的静态全局变量来保存该类的唯一实例 
            private static Singleton singleton;
            //定义一个只读静态对象 
            //且这个对象是在程序运行时创建的 
            private static readonly object syncObject = new object();
            /// <summary> 
            /// 构造函数必须是私有的 
            /// 这样在外部便无法使用 new 来创建该类的实例 
            /// </summary> 
            private Singleton(){}
            /// <summary> 
            /// 定义一个全局访问点 
            /// 设置为静态方法 
            /// 则在类的外部便无需实例化就可以调用该方法 
            /// </summary> 
            /// <returns></returns> 
            public static Singleton GetInstance() 
                { 
                //这里可以保证只实例化一次 
                //即在第一次调用时实例化 
                //以后调用便不会再实例化 
    
                //第一重 singleton == null 
                if (singleton == null) 
                { 
                    lock (syncObject) 
                    {
                        //第二重 singleton == null
                        if (singleton == null) 
                        { 
                            singleton = new Singleton(); 
                        } 
                    } 
                } 
                return singleton; 
            } 
        } 
    }

      上面的就是改进后的代码,可以看到在类中有定义了一个静态的只读对象  syncObject,这里需要说明的是,为何还要创建一个 syncObject 静态只读对象呢?由于提供给 lock 关键字的参数必须为基于引用类型的对象,该对象用来定义锁的范围,所以这个引用类型的对象总不能为 null 吧,而一开始的时候,singleton 为 null ,所以是无法实现加锁的,所以必须要再创建一个对象即 syncObject 来定义加锁的范围。

      还有要解释一下的就是在 GetInstance()中,我为什么要在 if 语句中使用两次判断 singleton == null , 这里涉及到一个名词 Double-Check Locking ,也就是双重检查锁定,为何要使用双重检查锁定呢?考虑这样一种情况,就是有两个线程同时到达,即同时调用 GetInstance(),此时由于 singleton == null ,所以很明显,两个线程都可以通过第一重的 singleton == null ,进入第一重 if 语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 singleton == null ,而另外的一个线程则会在 lock 语句的外面等待。而当第一个线程执行完 new  Singleton()语句后,便会退出锁定区域,此时,第二个线程便可以进入 lock 语句块,此时,如果没有第二重 singleton == null 的话,那么第二个线程还是可以调用 new  Singleton()语句,这样第二个线程也会创建一个 Singleton 实例,这样也还是违背了单例模式的初衷的,所以这里必须要使用双重检查锁定。

      细心的朋友一定会发现,如果我去掉第一重 singleton == null ,程序还是可以在多线程下完好的运行的,考虑在没有第一重 singleton == null 的情况下,当有两个线程同时到达,此时,由于 lock 机制的存在,第一个线程会进入 lock 语句块,并且可以顺利执行 new Singleton(),当第一个线程退出 lock 语句块时, singleton 这个静态变量已不为 null 了,所以当第二个线程进入 lock 时,还是会被第二重 singleton == null 挡在外面,而无法执行 new Singleton(),所以在没有第一重 singleton == null 的情况下,也是可以实现单例模式的?那么为什么需要第一重 singleton == null 呢?

      这里就涉及一个性能问题了,因为对于单例模式的话,new Singleton()只需要执行一次就 OK 了,而如果没有第一重 singleton == null 的话,每一次有线程进入 GetInstance()时,均会执行锁定操作来实现线程同步,这是非常耗费性能的,而如果我加上第一重 singleton == null 的话,那么就只有在第一次,也就是 singleton ==null 成立时的情况下执行一次锁定以实现线程同步,而以后的话,便只要直接返回 Singleton 实例就 OK 了而根本无需再进入 lock 语句块了,这样就可以解决由线程同步带来的性能问题了。

    下面将要介绍的是懒汉式单例和饿汉式单例

     懒汉式单例

       何为懒汉式单例呢,可以这样理解,单例模式呢,其在整个应用程序的生命周期中只存在一个实例, 懒汉式呢,就是这个单例类的这个唯一实例是在第一次使用 GetInstance()时实例化的,如果您不调用 GetInstance()的话,这个实例是不会存在的,即为 null, 形象点说呢,就是你不去动它的话,它自己是不会实例化的,所以可以称之为懒汉。其实呢,我前面在介绍单例模式的这几个 Demo 中都是使用的懒汉式单例,

     看下面的 GetInstance()方法就明白了:

     public static Singleton GetInstance() 
            { 
                if (singleton == null) 
                { 
                    lock (syncObject) 
                    {
                         if (singleton == null) 
                        { 
                            singleton = new Singleton(); 
                        } 
                    } 
                } 
                return singleton; 
            }

     从上面的这个 GetInstance()中可以看出这个单例类的唯一实例是在第一次调用 GetInstance()时实例化的,所以此为懒汉式单例。

     饿汉式单例

     上面介绍了饿汉式单例,到这里来理解懒汉式单例的话,就容易多了,懒汉式单例由于人懒,所以其自己是不会主动实例化单例类的唯一实例的,而饿汉式的话,则刚好相反,其由于肚子饿了,所以到处找东西吃,人也变得主动了很多,所以根本就不需要别人来催他实例化单例类的为一实例,其自己就会主动实例化单例类的这个唯一类。

     在 C# 中,可以用特殊的方式实现饿汉式单例,即使用静态初始化来完成饿汉式单例模式

     下面就来看一看饿汉式单例类

    namespace Singleton 
    { 
        public sealed class Singleton 
        { 
            private static readonly Singleton singleton = new Singleton();
            private Singleton(){}
            public static Singleton GetInstance() 
            { 
                return singleton; 
            } 
        } 
    }

      要先在这里提一下的是使用静态初始化的话,无需显示地编写线程安全代码,C# 与 CLR 会自动解决前面提到的懒汉式单例类时出现的多线程同步问题。上面的饿汉式单例类中可以看到,当整个类被加载的时候,就会自行初始化 singleton 这个静态只读变量。而非在第一次调用 GetInstance()时再来实例化单例类的唯一实例,所以这就是一种饿汉式的单例类。 

    好,到这里,就真正的把单例模式介绍完了,在此呢再总结一下单例类需要注意的几点:

      一、单例模式是用来实现在整个程序中只有一个实例的。

      二、单例类的构造函数必须为私有,同时单例类必须提供一个全局访问点。

      三、单例模式在多线程下的同步问题和性能问题的解决。

      四、懒汉式和饿汉式单例类。

      五、C# 中使用静态初始化实现饿汉式单例类。

  • 相关阅读:
    python中list/tuple/dict/set的区别
    jquery修改ajax的header的字段origin方法,均被浏览器拒绝
    js判断上传文件的大小、类型、修改日期等信息
    js调试方法
    sqlmapapi的跨域访问Access-Control-Allow-Origin:*;ajax
    flask的文件上传和下载
    flask中的g、add_url_rule、send_from_directory、static_url_path、static_folder的用法
    python读写csv时中文乱码问题解决办法
    css中!important的作用
    项目经验——Sql server 数据库的备份和还原____还原数据库提示“介质集有2个介质簇,但只提供了1个。必须提供所有成员” .
  • 原文地址:https://www.cnblogs.com/yyxt/p/4011388.html
Copyright © 2020-2023  润新知