• C++设计模式 -- 单例模式


    什么是单例模式

      顾名思义,就是只有一个实例的设计模式。比较专业的解释是:“保证一个类仅有一个实例,并提供一个该实例的全局访问点”。

      那么如何保证程序运行过程中,只有一个实例,就是单例模式的实现方法。

      而根据创建实现的时间不同,又可以把单例模式分为以下两类:

    • 懒汉式

        什么是懒汉式,核心就是“懒”,你不叫我,我就一动不动,纹丝不动。指不使用就不会去创建实例,使用时才创建。

        懒汉式,是在程序运行中创建,而程序运行,涉及到多线程时,就需要考虑到线程安全问题了。

    • 饿汉式

        什么是饿汉式,核心就是“饿”,你不叫我,我也动。指在程序一运行,就是初始创建实例,当需要时,直接调用。

        饿汉式,是在程序一运行,就创建好了,那时多线程还没有跑起来,因此不存在线程安全问题。

    单例模式的特点:

    •  private的构造函数与析构函数。目的就是禁止外部构造和析构。
    •     public的获取实例的静态函数。目的就是可以全局访问,用于获取实例。
    •     private的成员变量。目的也是禁止外部访问。

    根据单例模式的特点,现在就可以来使用代码实现了。

    PS.为了blog方便,把声明与实现都放在了.h文件中。

    CSingleton.h

     1 #pragma once
     2 
     3 #include <iostream>
     4 
     5 class CSingleton
     6 {
     7 private:
     8     CSingleton()   
     9     {
    10         std::cout << "构造" << std::endl;
    11     }
    12     ~CSingleton()  
    13     {
    14         std::cout << "析构" << std::endl;
    15     }
    16 
    17 public:
    18     static CSingleton* GetInstance()
    19     {
    20         if (!m_pInstance)
    21         {
    22             m_pInstance = new CSingleton();
    23         }
    24         return m_pInstance;
    25     }
    26 
    27 private:
    28     static CSingleton* m_pInstance;
    29 };
    30 
    31 CSingleton* CSingleton::m_pInstance = nullptr;

    单线程测试用例

     1 #include <iostream>
     2 #include "CSingleton.h"
     3 
     4 int main()
     5 {
     6     CSingleton* pInstance = CSingleton::GetInstance();
     7 
     8     std::cout << "pInstance地址:" << pInstance << std::endl;
     9 
    10     return 0;
    11 }

    结果如下:

     注意:析构函数是没有被调用的。

    根据使用时的第6行代码可以看出,此对像是在使用时才被构造出来,所以,为懒汉式的单例模式。

    既然是在使用中才进行构造 ,而使用时的环境也许会比较复杂,尤其是遇到多线程的情况时。

    那么,现在就模拟一下,多线程下,懒汉模式会出现什么情况 。

    多线程测试用例

     1 #include <windows.h>
     2 #include <process.h>
     3 #include "CSingleton.h"
     4 
     5 const int THREADNUM = 5;
     6 
     7 unsigned int __stdcall SingletonProc(void* pram)
     8 {
     9     CSingleton* pInstance = CSingleton::GetInstance();
    10 
    11     Sleep(50);
    12     std::cout << "pInstance:" << pInstance << std::endl;
    13 
    14     return 0;
    15 }
    16 
    17 int main()
    18 {
    19 
    20     HANDLE hHandle[THREADNUM] = {};
    21     int nCurThread = 0;
    22 
    23     while (nCurThread < THREADNUM)
    24     {
    25         hHandle[nCurThread] = (HANDLE)_beginthreadex(NULL, 0, SingletonProc, NULL, 0, NULL);
    26         nCurThread++;
    27     }
    28     WaitForMultipleObjects(THREADNUM, hHandle, TRUE, INFINITE);
    29 
    30     return 0;
    31 }

    从结果可以看出, 实际构造了5次,产生了5个实例。

    注意:析构函数也是没有被调用的。

    不仅如此,无论是多线程,还是单线程,似乎程序结束时,都没有调用析构函数。

    那么,我们来解决第一个问题 -- 析构函数调用问题。

    解决问题前,首先要了解问题出现的原因,那么析构函数没有被调用是为什么?

    可能有小伙伴会问,为什么不直接使用delete来释放呢?

    首先要注意一点,C++是属于静态绑定的语言。在编译期间,所有的非虚函数调用都必须分析完成。

    当在栈上生成对像时,对像会自动析构,也就是析构函数必须可以访问;

    当在堆上生成对像时,系统会将析构的时机交由程序员控制,而析构函数又为private,只能在类域内访问。

    因为,如果要释放空间,需要在类中添加函数,手动delete,最郁闷的是,程序那么大,怎么能确保实例使用完了,需要释放,

    又怎么确保下次使用的时候,实例没有被释放。。。。。如此,我们需要它自动释放。

     静态局变量,解决自动释放与线程问题

     1 class CSingleton
     2 {
     3 private:
     4     CSingleton()
     5     {
     6         std::cout << "构造" << std::endl;
     7     }
     8     ~CSingleton()
     9     {
    10         std::cout << "析构" << std::endl;
    11     }
    12 
    13 public:
    14     static CSingleton* GetInstance()
    15     {
    16         static CSingleton Instance;
    17         return &Instance;
    18     }
    19 
    20 };

     

    以上,通过局部静态变量解决了自动释放问题,同时,也不会出现线程问题。

    值得注意的是,C++0X以后,要求编译器保证内部静态变量的线程安全性,因此在支持c++0X的编译器中这种方法可以产生线程安全的单例模式,

    然而在不支持c++0X的编译器中,这种方法无法得到保证。

    那么,非局部静态变量怎么解决自动释放的问题呢?

    可以考虑使用一个类,来专门释放,前文也提及到了,析构函数为private,只在类域内访问,所以,此类也只能为属于单例类的成员类。

    成员类解决自动释放问题,非线程安全

    代码如下:

     1 #pragma once
     2 
     3 #include <iostream>
     4 #include <mutex>
     5 
     6 
     7 class CSingleton
     8 {
     9 private:
    10     CSingleton()
    11     {
    12         std::cout << "构造" << std::endl;
    13     }
    14     ~CSingleton()
    15     {
    16         std::cout << "析构" << std::endl;
    17         18     }
    19 
    20     class CGarbo
    21     {
    22     public:
    23         CGarbo()
    24         {
    25             std::cout << "成员构造" << std::endl;
    26         }
    27         ~CGarbo()
    28         {
    29             if (CSingleton::m_pInstance)
    30             {
    31                 delete CSingleton::m_pInstance;CSingleton::m_pInstance = nullptr;
    32             }
    33         }
    34     };
    35     static CGarbo Garbo;//定义的一个静态成员变量,程序结束时,会自动调用它的析构函数。而它的析构函数,调用了delete,系统会调用单例类的析构。
    36 public:
    37     static CSingleton* GetInstance()
    38     {
    39         if (!m_pInstance)
    40         {
    41             m_pInstance = new CSingleton();
    42         }
    43         return m_pInstance;
    44     }
    45 
    46 private:
    47     static CSingleton* m_pInstance;
    48 };
    49 
    50 CSingleton* CSingleton::m_pInstance = nullptr;
    51 CSingleton::CGarbo CSingleton::Garbo;

    好的,那么剩下来,只需要解决线程问题了。关于线程问题,很自然的就会想到锁,那就来加一把锁。

    成员类解决自动释放问题,线程安全 -- 但锁开销大啊

     1 #pragma once
     2 
     3 #include <iostream>
     4 #include <mutex>
     5 
     6 class CSingleton
     7 {
     8 private:
     9     CSingleton()
    10     {
    11         std::cout << "构造" << std::endl;
    12     }
    13     ~CSingleton()
    14     {
    15         std::cout << "析构" << std::endl;
    16     }
    17 
    18     class CGarbo
    19     {
    20     public:
    21         CGarbo()
    22         {
    23             std::cout << "成员构造" << std::endl;
    24         }
    25         ~CGarbo()
    26         {
    27             if (CSingleton::m_pInstance)
    28             {
    29                 delete CSingleton::m_pInstance;CSingleton::m_pInstance = nullptr;
    30             }
    31         }
    32     };
    33 public:
    34     static CSingleton* GetInstance()
    35     {
    36         m_mutex.lock();
    37         if (!m_pInstance)
    38         {
    39             m_pInstance = new CSingleton();
    40         }
    41         m_mutex.unlock();
    42         return m_pInstance;
    43     }
    44 
    45 private:
    46     static CSingleton* m_pInstance;
    47     static std::mutex m_mutex;
    48     static CGarbo Garbo;
    49 };
    50 
    51 CSingleton* CSingleton::m_pInstance = nullptr;
    52 std::mutex CSingleton::m_mutex;
    53 CSingleton::CGarbo CSingleton::Garbo;

     如此,我们解决了线程中出现多个实例的问题,秉持着折腾的原则,仔细看GetInstance()函数,无论实例存不存在,都会先锁住,而锁是比较消耗资源的操作,怎么办呢?

    那在锁之前,再判断 一下,如果为nullptr再锁,开始创建,否则直接返回,这样只在第一次创建操作时,会执行锁操作。这就是双重检查锁定模式(DCLP)。

    代码如下 

    成员类解决自动释放问题,线程安全? -- 锁开销较小

     1 #pragma once
     2 
     3 #include <iostream>
     4 #include <mutex>
     5 
     6 class CSingleton
     7 {
     8 private:
     9     CSingleton()   
    10     {
    11         std::cout << "构造" << std::endl;
    12     }
    13     ~CSingleton()  
    14     {
    15         std::cout << "析构" << std::endl;
    16     }
    17 
    18     class CGarbo
    19     {
    20     public:
    21         CGarbo()
    22         {
    23             std::cout << "成员构造" << std::endl;
    24         }
    25         ~CGarbo()
    26         {
    27             if (CSingleton::m_pInstance)
    28             {
    29                 delete CSingleton::m_pInstance;CSingleton::m_pInstance = nullptr;
    30             }
    31         }
    32     };
    33 public:
    34     static CSingleton* GetInstance()
    35     {
    36         if (!m_pInstance)
    37         {
    38             m_mutex.lock();
    39             if (!m_pInstance)
    40             {
    41                 m_pInstance = new CSingleton();
    42             }
    43             m_mutex.unlock();
    44         }
    45         return m_pInstance;
    46     }
    47 
    48 private:
    49     static CSingleton* m_pInstance;
    50     static std::mutex m_mutex;
    51     static CGarbo Garbo;
    52 };
    53 
    54 CSingleton* CSingleton::m_pInstance = nullptr;
    55 std::mutex CSingleton::m_mutex;
    56 CSingleton::CGarbo CSingleton::Garbo;

    好,这样减少了锁的开销,又保证了线程中唯一的一个实例,也自动释放,经过如此努力,真想给自己一个大写的 PERFECT。

    接下来,我得说两个字   “但是”,,,好的,相信这两个字都已经懂了,事情没有那么简单。下一篇会详细写出问题所在。

    到这里,我们已经花了不少的篇幅来让这只“懒虫”模式正常运行,那就先让满足它,先让它懒着吧。

    被凉在一边的饿汉模式已经够饿了,现在咱们去喂一喂。

     1 #pragma once
     2 
     3 #include <iostream>
     4 #include <mutex>
     5 
     6 class CSingleton
     7 {
     8 public:
     9     static CSingleton* GetInstance()
    10     {
    11         return m_pInstance;
    12     }
    13 private:
    14     CSingleton()
    15     {
    16         std::cout << "构造" << std::endl;
    17     };
    18     ~CSingleton()
    19     {
    20         std::cout << "析构" << std::endl;
    21     }
    22 
    23     
    24 private:
    25     static CSingleton* m_pInstance;
    26 };
    27 
    28 CSingleton* CSingleton::m_pInstance = new(std::nothrow)CSingleton;

    等等,析构又去哪儿了? 

    static是存放在全局数据区域中,显然存放的为一个实例对象指针,而真正占有资源的实例对象是存储在堆中的。同样需要主动地去释放,但它是私有的啊。那就使用懒汉的方式来试试。

    懒汉模式自动释

     1 #pragma once
     2 
     3 #include <iostream>
     4 #include <mutex>
     5 
     6 
     7 class CSingleton
     8 {
     9 public:
    10     static CSingleton* GetInstance()
    11     {
    12         return m_pInstance;
    13     }
    14 private:
    15     CSingleton()
    16     {
    17         std::cout << "构造" << std::endl;
    18     };
    19     ~CSingleton()
    20     {
    21         std::cout << "析构" << std::endl;
    22     }
    23 
    24     class CGarbo
    25     {
    26     public:
    27         CGarbo()
    28         {
    29             std::cout << "成员构造" << std::endl;
    30         }
    31         ~CGarbo()
    32         {
    33             if (CSingleton::m_pInstance)
    34             {
    35                 delete CSingleton::m_pInstance;
    36                 m_pInstance = nullptr;
    37             }
    38         }
    39     };
    40 private:
    41     static CSingleton* m_pInstance;
    42     static CGarbo Garbo;
    43 };
    44 
    45 CSingleton* CSingleton::m_pInstance = new(std::nothrow)CSingleton;
    46 CSingleton::CGarbo CSingleton::Garbo;

    此时,发现,自动析构了,不错,此时可以有一个大写的 PERFECT了。

    在使用多线程测试用例试试

    并未出现多个实例问题,为是什么呢?

    饿汉模式的对象在类产生时就创建了,所以线程在使用时,不会再进行创建,自然是安全的。

     简单的总结一下

    懒汉式:在使用时才会创建实例,空间消耗小,是一种时间换空间的方式。至于线程开锁的问题,DCLP基本可以解决。但DCLP在多线程中会存在一个有趣的问题,之后会单列出。

    饿汉式:在程序一开始就创建实例,空间开消相对懒汉式大,是一种空间换时间的方式。而且也不存在线程安全问题,效率会高上一些。

  • 相关阅读:
    php优秀框架codeigniter学习系列——安装,配置
    设计模式学习系列——桥接模式
    elasticsearch学习笔记——相关插件和使用场景
    elasticsearch学习笔记——安装,初步使用
    设计模式学习系列——适配器模式
    php优秀框架codeigniter学习系列——前言
    设计模式学习系列——原型模式
    angular 自定义指令 directive transclude 理解
    inq to datatable group by 多列 实现
    CSS3 media 入门
  • 原文地址:https://www.cnblogs.com/XavierJian/p/12388887.html
Copyright © 2020-2023  润新知