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在多线程中会存在一个有趣的问题,之后会单列出。
饿汉式:在程序一开始就创建实例,空间开消相对懒汉式大,是一种空间换时间的方式。而且也不存在线程安全问题,效率会高上一些。