zoukankan      html  css  js  c++  java
  • 【设计模式】C++单例模式的几种写法——Java自动加载内部类对象,C++怎么破?

    单例模式是最简单的设计模式,就让我像玩简单的游戏一样写下去吧。

    v1: 简单模式

    和这个版本有过一面之缘,但不敢苟同。

    class Singleton 
    {
        private:
        Singleton() {}
        public:
        static Singleton * getIns()
        {
            static Singleton * ins = new Singleton();
            return ins;
        }
    };

    问题:何时析构不明确;最重要的是调用多次getIns函数会产生多个static Singleton指针,指向每次都调用都new出来的实例。

    v2: 一般模式

    典型写法

    class Singleton 
    {
        private:
        Singleton() {}
        static Singleton * ins; 
        public:
        static Singleton * getIns()
        {
            if(!ins) ins  = new Singleton();
            return ins;
        }
    };
    
    static Singleton * Singleton::ins = NULL;

    问题:仍然未考虑析构问题;对象可能被复制出多个副本。

    Java中由于允许在调用构造函数之前先初始化变量,因此有这样一种写法:

    public class Singleton 
    {
        private Singleton() {}
        public static Singleton ins = new Singleton(); 
        public static Singleton * getIns()
        {
            return ins;
        }
    };

    简洁明了,也是蛮OK啦,析构也省了,并且由于初始化这个语句是JVM做的,因此人工的同步也省了(不带这么欺负C++程序员的 = =)。

    v3: 加强模式

    加入私有的复制构造函数以防出现单例对象的副本;加入一个内部静态类,整个程序结束后,静态类随着其他静态变量消亡,此时调用析构函数将ins析构。

    class Singleton 
    {
        private:
        Singleton() {}
        Singleton(const Singleton & s) {}
        Singleton & operator = (const Singleton & s) {}
    
        static Singleton * ins; 
    
        public:
        static Singleton * getIns()
        {
            if(!ins) ins  = new Singleton();
            return ins;
        }
    
        class CGarbo  // 内部类 
        {  
        public:  
            ~CGarbo()  
            {  
                if(Singleton::ins)  
                    delete Singleton::ins;  
            }  
        };  
        static CGarbo Garbo; 
    }; 
    static Singleton * Singleton::ins = NULL;

    问题:不是线程安全的。

    v4: hard模式

        static Singleton * getIns()
        {
            pthread_mutex_lock( &mutex_lock );
            if(!ins) ins  = new Singleton();
            pthread_mutex_unlock( &mutex_lock );
            return ins;
        }

    问题:不管ins是不是空,都要加锁,代价高

    v5: 再接再励模式

    当ins不为空时,反正getIns函数不管读写,直接返回就好了,读写锁尽可交给使用者。

        static Singleton * getIns()
        {
            if(!ins)
            {
                pthread_mutex_lock( &mutex_lock );
                if(!ins) ins = new Singleton();
                pthread_mutex_unlock( &mutex_lock );
            }
            return ins;
        }

    自然,getIns加了锁,析构的时候也同样要加锁。继续学习,再接再励!

    v6:虎牢关模式

    实际上,只有在第一次创建单例的时候需要获得锁。既然可以用一个内部类的静态实例实现单例对象的自动释放,何不把该静态对象的初始化也交由该内部类来维护呢:

    class Singleton
    {
    public:
        Singleton() {}
        Singleton(const Singleton &s) {}
        Singleton & operator = (const Singleton & s) {}
    
    public:
        static Singleton *getInstance()
        {
            return GC::pinstance;
        }
    
        class GC
        {
        public:
            static Singleton *pinstance;
            GC()
            {
                pinstance = new Singleton();
            }
            ~GC()
            {
                if( pinstance )
                {
                        delete pinstance; 
                }
            }
        };
        GC gc;
    };
    Singleton * Singleton::GC::pinstance = nullptr;

    注意,此处将Singleton的构造函数改成了public。因为在系统中没有Singleton实例时,Singleton的static成员也是不会被创建的,因此需将构造函数改成public,在使用词单例类时,先声明一个Singleton对象。

    如此,进程启动,在初始化阶段先初始化了不属于任何实例对象的Singleton的各个静态成员,顺便new好了我们所需的单例对象指针,这一过程不是线程来干的,因此这样的new操作是安全的。而在程序运行结束时,gc自动析构,将单例对象释放掉,这一过程也不是线程来干的,因此也是线程安全的,そして,至于pthread_mutex? WTF。而由于不管存在多少Singleton对象,每次getInstance必然获取到的是同一个instance。

     

    v7:  终极模式

    等等,所有“非你莫属”的类,都要改写成单例模式么?NONONO,能不能只写一个单例模式,所有需要限制实例个数的类都可复用之?这样,当系统中需要多个不同类的单例对象时,可以节约许多代码。当然可以做到,用模板就可以:

    template <typename T>
    class Singleton
    {
    public:
        Singleton() {}
        Singleton(const Singleton &s) {}
        Singleton & operator = (const Singleton & s) {}
    
    public:
        static Singleton *getInstance()
        {
            return GC::pinstance;
        }
    
        class GC
        {
        public:
            static T *pinstance;
            GC()
            {
                pinstance = new T();
            }
            ~GC()
            {
                if( pinstance )
                {
                        delete pinstance; pinstance = nullptr;
                }
            }
        };
        GC gc;
    };
    template <typename T> T
    * Singleton<T>::GC::pinstance = nullptr;

        当然很显然,使用了模板的话,就需要一些约定俗成的东西了,例如在实例化的时候需要一个不带参数的构造函数T()。在使用时,对于用Singleton<T>::getInstance函数返回得到的对象指针,就是非你莫属的同一个指针了:

    int main()
    {
    Singleton<int> sgton;
    int *p1 = Singleton<int>::getInstance(); int *p2 = Singleton<int>::getInstance(); int *p3 = Singleton<int>::getInstance();
    printf("%x ", p1); cout
    <<(p1==p2)<<endl; cout <<(p2==p3)<<endl; return 0; }

       以上测试代码的结果为0x........ 1 1.  

       ちょっと待って!!还不够!以上代码还是有问题的!注意此时我们要限制对象个数的不是Singleton对象了,而是T对象。因此设计目标是:不管申明多少个Singleton对象都是合法的,而不管申明了多少个Singleton对象,调用getInstance都只会得到一个T对象。

        再看上面的代码,如果用户手贱声明了多个Singleton对象会怎么样呢?对于一个Singleton类的static对象,只要程序中的Singleton对象个数大于0,就会维护唯一的一个static变量。可是上面的static变量是个指针!!这就意味着每次构造Singleton对象时,都会调用GC构造函数中的new运算符,将一个新的T对象的地址赋值给pinstance!!那旧的那个呢?WHO CARES!!这样一来轻则导致内存泄漏,重则导致系统运行出错——如果先申明了一个Singleton对象,用着用着中间又声明出来一个Singleton对象,那么pinstance指向的内容就被偷换了!

        说了这么多,为了防止发生这样的事情,只需要在new的时候和delete的时候稍稍注意一下即可,为了防止new两次,加上if语句;为了防止delete两次(当然在其他场合也一样),养成delete之后就将指针赋为空,以免指针在其他地方被误引用。

    template <typename T>
    class Singleton
    {
    public:
        Singleton() {}
        Singleton(const Singleton &s) {}
        Singleton & operator = (const Singleton & s) {}
    
    public:
        static T *getInstance()
        {
            return GC::pinstance;
        }
    
        class GC
        {
        public:
            static T *pinstance;
            GC()
            {
                    if( !pinstance ) pinstance = new T();
            }
            ~GC()
            {
                if( pinstance )
                {
                        delete pinstance; pinstance = nullptr;
    } } };
    static GC gc; }; template <typename T> T * Singleton<T>::GC::pinstance = nullptr;

     这样一来,只要在起子线程之前声明一个Singleton对象,那么不管后面是main线程还是子线程声明了其他的Singleton对象,T对象都不会出现第二个。

    反思:

    1  为什么Java用内部类维护单例?Java这么做是为了在不写synchronized关键字(减少锁的代价)的情况下,采用延时加载的方式实现单例。怎么做到不用锁而做到线程安全呢?在Java中,内部类都只会加载一次,因此内部类的初始化过程有且仅有一次,而这个过程是JVM干的,不受用户多线程的影响。为什么是延时加载呢?只有getInstance()用到了内部类的时候,才会加载内部类。

    2  为什么C++写法和Java延时加载且线程安全的单例写法不一样呢?C++是不会加载内部类的,只有当有对象需要实例化时,才调用其构造函数进行初始化,同时也初始化内部类的static变量。如果像Java一样只是定义一个内部类而不去实例化它,是不行的。因此按照上面的写法,使用该单例模板类必须先定义一个Singleton对象,这时候由于内部类作为Singleton的成员,会先被构造,“顺便地”就正确地new出了一个T对象,pinstance指针是static变量,一个类最多只有一个,所以不会有两个pinstance,那么只要再内部类的构造中防止new出两个T对象,就可以保证进程中有唯一的pinstance指向唯一的T。

    to be continue

  • 相关阅读:
    解决死锁四大方式
    Windows内存管理简介:
    排序算法优劣
    排序
    HTTPs
    http和https的异同
    HTTP协议
    FTP与TFTP
    tomcat热部署
    开发心得体会
  • 原文地址:https://www.cnblogs.com/zhchngzng/p/4328584.html
Copyright © 2011-2022 走看看