• C++ 单例模式的两种实现(包括一种饿汉式和两种懒汉式)


    一、核心概念

    1.单例模式的作用:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    2.单例模式的三大基本要求:

            1.私有化构造函数、拷贝构造函数、赋值运算符,防止多个实例被初始化

            2.类内进行私有静态实例定义

            3.类内提供一个公有化访问实例的接口

    3.单例模式的类型

            1.饿汉式:在类加载的时候就已经创建好对象放在静态存储区

                    优点:类加载初始化,也就是在main()函数之前就已经完成了初始化,保证了线程安                        全,资源访问效率高

                    缺点:提前占用内存资源,降低类加载的效率和资源的利用率

            2.懒汉式:在实例被调用的时候再初始化唯一实例

                    优点:调用时初始化,节省资源

                    缺点:调用时初始化可能处于多线程运行环境,需要注意线程安全问题,资源获取效率                    较低

    4. =delete:显式禁止类成员函数的调用,在单例模式中用于禁止构造函数、拷贝构造函数、赋值运算符的调用,防止多个类实例的出现

    二、数据类

    数据类是为了观察单例模式在程序结束后有没有进行合理的资源释放

    1. // 数据类,用来检测实例的创建
    2. class Data {
    3. public:
    4. Data() { cout << "Data() 默认构造" << endl; }
    5. Data(const Data& src) { cout << "Data() 拷贝构造" << endl; }
    6. ~Data() { cout << "Data() 默认析构" << endl; }
    7. };

    三、两种实现

    1.最简单的实现方式,即不用双重检测加锁保证线程安全,也不需要使用atomic方法保证单例构造的原子性,因为C++11对局部静态变量的构造过程的原子性(为何要保证这个?下一种实现里面会说明)进行了保证,而全局静态变量的初始化在主函数,也就是线程开启之前,因此也是线程安全的,所以直接采用下面的代码。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. using namespace std;
    7. /*
    8. 最简单的实现,利用C++11静态局部变量初始化的线程安全性
    9. */
    10. // 懒汉式
    11. class Singleton{
    12. private:
    13. Singleton() {
    14. data = new Data();
    15. }
    16. ~Singleton() {
    17. delete data;
    18. data = nullptr;
    19. }
    20. Singleton(const Singleton&)= delete;// 私有化拷贝构造
    21. Singleton& operator=(const Singleton&)= delete;// 私有化赋值运算符
    22. Data* data;
    23. public:
    24. static Singleton& getInstance() { // 被引用时才初始化,且由于是局部静态变量,多次引用时仅分配一次空间
    25. static Singleton instance; // C++11规定了local static在多线程条件下的初始化行为,要求编译器保证了内部静态变量的线程安全性。
    26. return instance;
    27. }
    28. };
    29. //饿汉式
    30. class Singleton {
    31. private:
    32. Singleton() { data = new Data();}
    33. ~Singleton() {
    34. delete data;
    35. data = nullptr;
    36. }
    37. Singleton(const Singleton&)= delete;
    38. Singleton& operator=(const Singleton&) = delete;
    39. Data* data;
    40. static Singleton instance;
    41. public:
    42. static Singleton& getInstance() {
    43. return instance;
    44. }
    45. };
    46. Singleton Singleton::instance; // 声明并初始化静态成员
    47. void myThread() {
    48. Singleton::getInstance();
    49. }
    50. int main() {
    51. // 用横杠来验证单例构造是在main()函数之前还是之后
    52. // 如果是饿汗式,会在main函数之前,懒汉式则在第一次引用之后
    53. cout << "========" << endl;
    54. // 用多个线程去调用该实例测试线程安全性
    55. thread t1(&myThread);
    56. thread t2(&myThread);
    57. thread t3(&myThread);
    58. t1.join();
    59. t2.join();
    60. t3.join();
    61. return 0;
    62. }

    2.在实现代码之前,分析几种情况和解决方式

    1.线程安全:懒汉式在调用时初始化,也就是main()函数之后,如果是多线程的程序,那么可能带来

            举个例子:线程A第一次调用,检测到没有类实例,执行类实例的构造,还没构造完成线程B切换进来,也检测到没有类实例,又执行一次类实例的构造,导致了多个实例的产生

            解决方式:加锁,但只是检测完没有实例之后加锁构造,还是会有问题

            举个例子:线程A第一次调用,检测到没有实例,在加锁之前,线程B切换进来,也检测到没有实例,然后加锁(此时A在等待锁资源),进行实例构造,然后切换到A,A拿到了锁资源,又进行实例构造,造成了多个实例的构造,因此在加锁之后的代码里,要再检测一次是否已经存在了类实例

            解决方式:双重检测加锁,这样还是不够

            举个例子:由于某些内存模型或者编译器运行优化,可能会造成在线程A构造类实例的时候,new返回了地址赋值给实例指针,但实例构造未完成的情况,另一个线程B切换进来,访问的时候,检测到类指针 != nullptr,于是返回了一个未构造完成的对象,造成错误

            解决方式:atmoic保证对象操作的原子性,不同线程只能获取对象修改前或者修改后的值,无法获取修改过程中的对象

    2.资源泄露:因为我们不可能在单例类的析构函数中delete单例类指针,因为这就造成了析构的循环调用,delete类指针就要调用类的析构函数,而析构函数里又在执行delete类指针(如果不理解自己试一下就知道了),所以我们申请的实例资源在程序结束后并不会被释放

    解决方式:嵌套类,在类内定义相同生命周期的静态嵌套类,由嵌套类内部的析构函数来调用单例类的析构函数释放资源

    3.终于能够上代码了!!以上问题的解决都体现在了代码里,具体方法的使用可以再进行百度!!

    1. /*
    2. 懒汉式使用双重检查加锁机制保证线程安全,使用atomic保证对象构造操作的原子性,使用嵌套类负责释放单例对象
    3. */
    4. class Singleton{
    5. private:
    6. Singleton() {
    7. data = new Data();
    8. }
    9. ~Singleton() {
    10. delete data;
    11. data = nullptr;
    12. }
    13. Singleton(const Singleton&) = delete;// 私有化拷贝构造,并显式禁止调用
    14. Singleton& operator=(const Singleton&) = delete;// 私有化赋值运算符,并显式禁止调用
    15. class Release {
    16. public: // 注意:这个public一定要加!!!
    17. ~Release() {
    18. if (Singleton::instance != nullptr) {
    19. delete instance;
    20. instance = nullptr;
    21. cout << "嵌套类析构, 资源释放" << endl;
    22. }
    23. };
    24. };
    25. private:
    26. static Release release; // 释放资源的嵌套类,让程序结束时能够自动释放资源,也要设置为静态成员,生命周期和单例一致!!
    27. Data* data; // 数据
    28. static atomic instance; // 定义为原子变量
    29. static mutex m_utex; // 全局锁
    30. public:
    31. static Singleton* getInstance() { // 被引用时才初始化,且由于是局部静态变量,多次引用时仅分配一次空间
    32. Singleton* tmp = instance.load();
    33. if (tmp == nullptr) {
    34. lock_guardlock(m_utex);
    35. tmp = instance.load();
    36. cout << "加锁" << endl;
    37. if (tmp == nullptr) {
    38. cout << "初始化" << endl;
    39. tmp = new Singleton();
    40. instance.store(tmp);
    41. }
    42. }
    43. return tmp;
    44. }
    45. };
    46. atomic Singleton::instance; //类外声明静态成员(这里并非初始化!!!所以依然是懒汉模式)
    47. mutex Singleton::m_utex;
    48. Singleton::Release Singleton::release;
    49. void myThread() {
    50. Singleton::getInstance();
    51. cout << "调用实例" << endl;
    52. }
    53. int main() {
    54. // 用横杠验证单例构造是在main()函数之前还是之后
    55. // 如果是饿汗式,会在main函数之前,懒汉式则在第一次引用之后
    56. cout << "========" << endl;
    57. thread t1(&myThread);
    58. //Sleep(1000); // 加1s之后就不会再加锁,不加的话由于线程切换会重复加锁,但只调用一次构造函数
    59. thread t2(&myThread);
    60. thread t3(&myThread);
    61. thread t4(&myThread);
    62. thread t5(&myThread);
    63. t1.join();
    64. t2.join();
    65. t3.join();
    66. t4.join();
    67. t5.join();
    68. return 0;
    69. }

    作笔记使用,如果有讲的不对或者代码不够精妙甚至错误的地方,还请多多指正!如果觉得有帮助也可以三连一手哈哈哈,谢谢!!! 

  • 相关阅读:
    速锐得解码特斯拉Model Y整车网关电路及CAN通信协议DBC控制策略
    RK3399的Ubuntu计算机安装使用
    每日汇评:黄金形态确认牛市,再次尝试上行2000美元
    react高频面试题总结(附答案)
    【无标题】纯纯小白~学习python记录~用Django创建第一个应用
    Idea显示无法自动装配。找不到‘ xxx’类型的Bean
    写一个简单的网站步骤
    一文学会linux vim操作
    go语言中Map的使用
    小程序多文件上传 Tdesign
  • 原文地址:https://blog.csdn.net/weixin_44178960/article/details/126033100