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在多线程中会存在一个有趣的问题,之后会单列出。

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

猜你喜欢

转载自www.cnblogs.com/XavierJian/p/12388887.html