1. 单例模式(Singleton Pattern)
(1)定义:保证一个类仅有一个实例,同时提供能对该实例加以访问的全局访问方法。
(2)解决思路:
①在类中,要构造一个实例,就必须调用类的构造函数。如此,为了防止在外部调用类的构造函数而创建实例,需要将构造函数的访问权限设为protected或private;
②最后,需要提供全局访问点,就需要在类中定义一个static函数,返回在类内部唯一构造的实例。
2. 单例模式的实现
(1)懒汉式:
①其特点是延迟加载,典型的以“时间换空间”做法。即只会在全局访问方法首次被调用时才被创建。
②问题:new出来的实例不能自动释放,可能存在内存泄漏问题。其解决方法有2种。
A.方法1:在单例类内部定义专有的嵌套类,并在单例类定义一个私有的专门用于释放的静态成员,当程序结束析构全局变量时,会自动调用并释放这个单例。
B.方法2:在单例类提供一个专门用于释放的静态成员函数,并在需要时手动释放。
③在实际项目中,单例一般是个全局变量,它的生命期随软件运行结束而自然终止。也就没有内存泄漏,但是如果单例中用到了一个文件锁、文件句柄或数据库连接,这些并不会随程序的关闭而立即关闭,必须在程序关闭前进行手动释放。
//懒汉式1
//创建型模式:单例模式 #include <stdio.h> //懒汉式1:缺点,须手工释放 class CSingleton { public: //提供全局访问的访问点 static CSingleton* getInstance() { if(m_pInstance == NULL) { m_pInstance = new CSingleton(); } return m_pInstance; } //清除:须手工调用 static void clearInstance() { if(m_pInstance != NULL) { delete m_pInstance; m_pInstance = NULL; } } private: //将构造函数设为私有属性 CSingleton(){}; //把复制构造函数和=操作符也设为私有,防止被复制 CSingleton(const CSingleton&){} CSingleton& operator=(const CSingleton&){} static CSingleton* m_pInstance; }; CSingleton* CSingleton::m_pInstance = NULL; int main() { CSingleton* s1 = CSingleton::getInstance(); CSingleton* s2 = CSingleton::getInstance(); printf("s1 = %p ", s1); printf("s2 = %p ", s2); CSingleton::destroyInstance(); return 0; }
//懒汉式2
//创建型模式:单例模式 #include <stdio.h> //懒汉式2:可自动回收垃圾 class CSingleton { public: //提供全局访问的访问点 static CSingleton* getInstance() { if(m_pInstance == NULL) { m_pInstance = new CSingleton(); } return m_pInstance; } private: //将构造函数设为私有属性 CSingleton(){}; static CSingleton* m_pInstance; //内部类,用于垃圾回收 class GC { public: ~GC() { //在这里销毁资源,比如数据库连接,句柄等。 if(m_pInstance != NULL) { delete m_pInstance; m_pInstance = NULL; printf("test: ~GC "); } } }; static GC gc; }; //静态成员变量的初始化 CSingleton* CSingleton::m_pInstance = NULL; //全局静态变量,会被自动销毁,从而实现对单例的垃圾回收 CSingleton::GC CSingleton::gc; int main() { CSingleton* s1 = CSingleton::getInstance(); CSingleton* s2 = CSingleton::getInstance(); printf("s1 = %p ", s1); printf("s2 = %p ", s2); return 0; }
(2)饿汉式:其特点是一开始就加载了,典型的“空间换时间”作法。因为一开始就创建了实例,所以每次用时直接返回就好了
//创建型模式:单例模式 #include <stdio.h> //饿汉式:多线程安全 class CSingleton { public: //提供全局访问的访问点 static CSingleton* getInstance() { static CSingleton instance; return &instance; } private: //将构造函数设为私有属性 CSingleton(){}; }; int main() { CSingleton* s1 = CSingleton::getInstance(); CSingleton* s2 = CSingleton::getInstance(); printf("s1 = %p ", s1); printf("s2 = %p ", s2); return 0; }
(3)双重检查
//创建型模式:单例模式 #include <stdio.h> #include <windows.h> class Lock { private: HANDLE m_hMutex; public: Lock(){m_hMutex = CreateMutex(NULL, FALSE, NULL);} ~Lock(){CloseHandle(m_hMutex);} void lock(){WaitForSingleObject(m_hMutex, INFINITE);} void unlock(){ReleaseMutex(m_hMutex);} }; //多线程下的单例模式:处理懒汉式,因为饿汉式是线程安全的 class CSingleton { public: //提供全局访问的访问点 static CSingleton* getInstance() { //双检查 if(m_pInstance == NULL) //第1次检查 { m_lock.lock(); if(m_pInstance == NULL) //第2次检查 { m_pInstance = new CSingleton; } m_lock.unlock(); } return m_pInstance; } private: //将构造函数设为私有属性 CSingleton(){}; static CSingleton* m_pInstance; static Lock m_lock; }; CSingleton* CSingleton::m_pInstance = NULL; Lock CSingleton::m_lock; //线程函数,用来测试的 DWORD WINAPI ThreadProc(PVOID pvParam) { CSingleton* s = CSingleton::getInstance(); printf("thread:%d, address = %p ",(int)pvParam, s); Sleep(1000); return 0; } int main() { const int iCount = 64; HANDLE hThread[iCount]; for(int i = 0; i< iCount; i++) { hThread[i] = CreateThread(NULL,0,ThreadProc,(LPVOID)i,0,NULL); } //注意:WaitForMultipleObjects最多能等待64个内核对象 WaitForMultipleObjects(iCount, hThread, TRUE, INFINITE); for(int i = 0; i< iCount; i++) CloseHandle(hThread[i]); return 0; }
3. 单例模式的应用
(1)单例模式的优点
①由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
②由于单例模式只生成一个实例,所以减少了系统性能开销,当一个对象的产生需要较多资源时,可以启用一个单例对象然后长驻内存又节约内存空间。
③单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
④单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
(2)单例模式的缺点
①单例模式一般没有接口,扩展很困难。因其“自动实例化”,而抽象类和接口是不能被实例化的。所以不能增加接口
②单例模式对测试不利。在并行的开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也就不能虚拟一个对象。
(3)使用的注意事项
①首先在高并发的情况下,要注意单例模式的线程同步问题。
②单例类一般不会主动要求被复制的,因此复制构造函数和赋值构造函数一般也设为私有。
4. 单例模式的使用场景
(1)多线程之间共享一个资源或者操作同一个对象
(2)在整个程序空间中使用全局变量,共享资源
(3)在创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源。
(4)大规模系统中,为了性能的考虑,需要节省对象的创建时间
(5)要求生成唯一序列号的环境或用单例做为一个计数器。
5. 单例模式的扩展
(1)可以在单例类,定义产生实例的数量和列表,来控制创建的实例数量,这叫多例模式
(2)多例是单例的一种扩展,采用有上限的多例模式,可以在设计时决定内存中有多少个实例,以修正单例可能存在的性能问题,提高系统的响应速度。