内容
- 单例模式简介
- 饿汉式单例模式
- 懒汉式单例模式
- 线程安全的单例模式
单例模式简介
单例模式指的是,无论怎么获取,永远只能得到该类类型的唯一一个实例对象,那么设计一个单例就必须要满足下面三个条件:
- 构造函数私有化,这样用户就不能任意定义该类型的对象了
- 定义该类型唯一的对象
- 通过一个static静态成员方法返回唯一的对象实例
饿汉式(本身线程安全)
饿汉式单例模式,顾名思义,就是程序启动时就实例化了该对象,并没有推迟到第一次使用该对象时再进行实例化;如果运行过程中没有使用到,该实例对象就被浪费掉了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class CSingleton { public: static CSingleton* getInstance() { return &single; } private: static CSingleton single; CSingleton() { cout << "CSingleton()" << endl; } ~CSingleton() { cout << "~CSingleton()" << endl; } CSingleton(const CSingleton&) = delete; }; CSingleton CSingleton::single;
int main() { CSingleton *p1 = CSingleton::getInstance(); CSingleton *p2 = CSingleton::getInstance(); CSingleton *p3 = CSingleton::getInstance(); cout << p1 << " " << p2 << " " << p3 << endl; return 0; }
|
打印0016E138 0016E138 0016E138
可以看到,三次获取的CSingleton对象都是同一个对象实例,这是一个饿汉式单例模式。
懒汉式(线程不安全)
懒汉式单例模式,顾名思义,将对象的实例化延迟到第一次使用它的时刻。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class CSingleton { public: static CSingleton* getInstance() { if (nullptr == single) { single = new CSingleton(); } return single; } private: static CSingleton *single; CSingleton() { cout << "CSingleton()" << endl; } ~CSingleton() { cout << "~CSingleton()" << endl; } CSingleton(const CSingleton&) = delete; }; CSingleton* CSingleton::single = nullptr;
int main() { CSingleton *p1 = CSingleton::getInstance(); CSingleton *p2 = CSingleton::getInstance(); CSingleton *p3 = CSingleton::getInstance(); cout << p1 << " " << p2 << " " << p3 << endl; return 0; }
|
符合单例模式的要求,三次获取的都是同一个对象,而且程序启动时,只对single指针初始化了空值,等第一次调用getInstance
函数时,由于single指针为nullptr
,才进行对象的实例化,所以是一个懒汉式单例模式。
没有释放单例对象 - 内存泄漏
但是上面的代码有一个问题:只有new没有delete!作为C++
开发者,资源的分配和回收,我们必须要考虑清楚,不能糊涂。
这个问题确实棘手,首先资源的释放如果交给用户来操作,难免会忘记写delete,又或者多次delete,可能错误释放野指针。我们可以利用static静态对象在程序结束时自动析构这么一个特征,给出如下释放资源的代码:在单例类中定义一个嵌套类,在嵌套类的析构函数中,自动释放外层类的资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class CSingleton { public: static CSingleton* getInstance() { if (nullptr == single) { single = new CSingleton(); } return single; } private: static CSingleton *single; CSingleton() { cout << "CSingleton()" << endl; } ~CSingleton() { cout << "~CSingleton()" << endl; } CSingleton(const CSingleton&) = delete;
class CRelease { public: ~CRelease() { delete single; } }; static CRelease release; }; CSingleton* CSingleton::single = nullptr; CSingleton::CRelease CSingleton::release;
int main() { CSingleton *p1 = CSingleton::getInstance(); CSingleton *p2 = CSingleton::getInstance(); CSingleton *p3 = CSingleton::getInstance(); cout << p1 << " " << p2 << " " << p3 << endl; return 0; }
|
线程安全的懒汉式 - 锁
在开发服务器程序的时候,经常会用到多线程,多线程要考虑代码的线程安全特性,不能让代码在多线程环境下出现竞态条件,否则就要进行线程互斥操作,我们来考虑一下上面两种单例模式,如果用在多线程环境当中,是否是线程安全的单例模式。
- 饿汉单例模式的线程安全特性
- 饿汉单例模式中,单例对象定义成了一个static静态对象,它是在程序启动时,main函数运行之前就初始化好的,因此不存在线程安全问题,可以放心在多线程环境中使用。
- 懒汉单例模式的线程安全特性
- 懒汉单例模式,获取单例对象的方法如下:
1 2 3 4 5 6 7 8
| static CSingleton* getInstance() { if (nullptr == single) { single = new CSingleton(); } return single; }
|
很明显,这个getInstance
是个不可重入函数,也就它在多线程环境中执行,会出现竞态条件问题,首先搞清楚这句代码,single = new CSingleton()
它会做三件事情,开辟内存,调用构造函数,给single指针赋值,那么在多线程环境下,就有可能出现如下问题:
- 线程A先调用getInstance函数,由于single为
nullptr
,进入if语句
- new操作先开辟内存,此时A线程的CPU时间片到了,切换到B线程
- B线程由于single为
nullptr
,也进入if语句了,开始new操作
很明显,上面两个线程都进入了if语句,都试图new一个新的对象,不符合单例模式的设计,那该如何处理呢?应该为getInstance函数内部加锁,在线程间进行互斥操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| #include <iostream> #include <pthread.h> using namespace std;
class CSingleton { public: static CSingleton* getInstance() { pthread_mutex_lock(&mutex); if (nullptr == single) { single = new CSingleton(); } pthread_mutex_unlock(&mutex); return single; } private: static CSingleton *single; CSingleton() { cout << "CSingleton()" << endl; } ~CSingleton() { pthread_mutex_destroy(&mutex); cout << "~CSingleton()" << endl; } CSingleton(const CSingleton&); class CRelease { public: ~CRelease() { delete single; } }; static CRelease release; static pthread_mutex_t mutex; }; CSingleton* CSingleton::single = nullptr; CSingleton::CRelease CSingleton::release;
pthread_mutex_t CSingleton::mutex = PTHREAD_MUTEX_INITIALIZER;
int main() { CSingleton *p1 = CSingleton::getInstance(); CSingleton *p2 = CSingleton::getInstance(); CSingleton *p3 = CSingleton::getInstance(); return 0; }
|
上面的代码,是一个线程安全的懒汉单例模式,但是效率太低,因为每次调用getInstance都需要加锁解锁,除了第一次调用,后面对getInstance函数持续的加解锁实在时没有必要,所以这里需要使用锁+双重判断
,也叫双重检验锁,把上面的getInstance函数代码修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| static CSingleton* getInstance() { if (nullptr == single) { pthread_mutex_lock(&mutex);
if(nullptr == single) { single = new CSingleton(); } pthread_mutex_unlock(&mutex); }
return single; }
|
如果把mutex封装为一个类,那么更加符合面向对象的思想。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| #include <iostream> #include <pthread.h> using namespace std;
class CMutex { public: CMutex(){pthread_mutex_init(&mutex, NULL);} ~CMutex(){pthread_mutex_destroy(&mutex);} void lock(){pthread_mutex_lock(&mutex);} void unlock(){pthread_mutex_unlock(&mutex);} private: pthread_mutex_t mutex; };
class CSingleton { public: static CSingleton* getInstance() { if (nullptr == single) { mutex.lock();
if(nullptr == single) { single = new CSingleton(); } mutex.unlock(); }
return single; } private: static CSingleton *single; CSingleton() { cout << "CSingleton()" << endl; } ~CSingleton() { cout << "~CSingleton()" << endl;} CSingleton(const CSingleton&);
class CRelease { public: ~CRelease() { delete single; } }; static CRelease release;
static CMutex mutex; }; CSingleton* CSingleton::single = nullptr; CSingleton::CRelease CSingleton::release;
CMutex CSingleton::mutex;
int main() { CSingleton *p1 = CSingleton::getInstance(); CSingleton *p2 = CSingleton::getInstance(); CSingleton *p3 = CSingleton::getInstance(); return 0; }
|
输出结果:
1 2
| CSingleton() ~CSingleton()
|
线程安全的懒汉式 - call_once
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #include <iostream> #include <mutex> using namespace std; class CSingleton { public: static CSingleton* getInstance() { std::call_once(once_flag, init); return single; } private: static void init() { single = new CSingleton; } static CSingleton * single; static std::once_flag once_flag; CSingleton() { cout << "CSingleton()" << endl; } ~CSingleton() { cout << "~CSingleton()" << endl; } CSingleton(const CSingleton&) = delete; CSingleton& operator=(const CSingleton&) = delete; class CRelease { public: ~CRelease() { delete single; } }; static CRelease release; };
CSingleton* CSingleton::single = nullptr; std::once_flag CSingleton::once_flag; CSingleton::CRelease CSingleton::release; int main() { CSingleton* p1 = CSingleton::getInstance(); CSingleton* p2 = CSingleton::getInstance(); CSingleton* p3 = CSingleton::getInstance(); return 0; }
|
运行结果:
1 2
| CSingleton() ~CSingleton()
|
更简洁的线程安全的懒汉式
下面这个懒汉单例模式是否是线程安全的,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <iostream> using namespace std;
class CSingleton { public: static CSingleton* getInstance() { static CSingleton single; return &single; } private: CSingleton() { cout << "CSingleton()" << endl; } ~CSingleton() { cout << "~CSingleton()" << endl; } CSingleton(const CSingleton&) = delete; }; int main() { CSingleton *p1 = CSingleton::getInstance(); CSingleton *p2 = CSingleton::getInstance(); CSingleton *p3 = CSingleton::getInstance(); return 0; }
|
上面的单例模式在多线程环境中使用时,会不会出现这种情况,线程A第一次调用getInstance函数的时候,single对象第一次初始化,此时线程B也调用getInstance函数,会不会也进行single对象的初始化呢,因为此时线程A并没有初始化完single?
在Linux环境中,通过g++编译上面的代码,命令如下:
g++ -o main main.cpp -g
生成可执行文件main,用gdb进行调试,到getInstance函数,并打印该函数的汇编指令,如下:

可以看到,对于static静态局部变量的初始化,编译器会自动对它的初始化进行加锁和解锁控制,使静态局部变量的初始化成为线程安全的操作,不用担心多个线程都会初始化静态局部变量,因此上面的懒汉单例模式是线程安全的单例模式!
总结
一个看似小小的单例模式,却可以串联面向对象思想,到软件设计思想,到设计模式,再到Linux操作系统的进程和线程模型,线程间的互斥和通信。
参考文献:C++设计模式 - 单例模式_单例模式大秦坑王-CSDN博客