zoukankan      html  css  js  c++  java
  • 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在多线程中会存在一个有趣的问题,之后会单列出。

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

  • 相关阅读:
    使用MobaXterm远程连接Ubuntu,启动Octave,界面不能正常显示
    ABP .Net Core 日志组件集成使用NLog
    ABP .Net Core Entity Framework迁移使用MySql数据库
    ABP前端使用阿里云angular2 UI框架NG-ZORRO分享
    阿里云 Angular 2 UI框架 NG-ZORRO介绍
    Visual Studio 2019 Window Form 本地打包发布猫腻
    VS Code + NWJS(Node-Webkit)0.14.7 + SQLite3 + Angular6 构建跨平台桌面应用
    ABP .Net Core 调用异步方法抛异常A second operation started on this context before a previous asynchronous operation completed
    ABP .Net Core To Json序列化配置
    .Net EF Core数据库使用SQL server 2008 R2分页报错How to avoid the “Incorrect syntax near 'OFFSET'. Invalid usage of the option NEXT in the FETCH statement.”
  • 原文地址:https://www.cnblogs.com/XavierJian/p/12388887.html
Copyright © 2011-2022 走看看