“请用C++写一个单例,考虑一下多线程环境。”
这是一个常见的面试题,别人问过我,我也问过别人。
这个问题可以很简单,也可以很复杂。
简单有效的单例
1 2 3 4 5 6 7
| class Singleton { public: static Singleton* GetInstance() { Singleton singleton; return &singleton; } };
|
在C++11中静态局部变量的初始化是线程安全的,参考链接。
这种写法既简单,又是线程安全的,可以满足大多数场景的需求。
饿汉模式
单例在程序初期进行初始化。即如论如何都会初始化。
1 2 3 4 5 6 7 8 9 10
| class Singleton { public: static Singleton* GetInstance() { return singleton; } static Singleton* singleton; }; Singleton* Singleton::singleton = new Singleton();
|
这种写法也是线程安全的,不过Singleton的构造函数在main函数之前执行,有些场景下是不允许这么做的。改进一下:
1 2 3 4 5 6 7 8 9 10 11 12
| class Singleton { public: static Singleton* GetInstance() { return singleton; } int Init(); static Singleton* singleton; }; Singleton* Singleton::singleton = new Singleton();
|
将复杂的初始化操作放在Init函数中,在主线程中调用。
懒汉模式
单例在首次调用时进行初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Singleton { public: static Singleton* GetInstance() { if (singleton == NULL) { singleton = new Singleton(); } return singleton; } static Singleton* singleton; }; Singleton* Singleton::singleton = NULL;
|
这样写不是线程安全的。改进一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Singleton { public: static Singleton* GetInstance() { lock(); if (singleton == NULL) { singleton = new Singleton(); } unlock(); return singleton; } static Singleton* singleton; }; Singleton* Singleton::singleton = NULL;
|
这样写虽是线程安全的,但每次都要加锁会影响性能。
DCLP(Double-Checked Locking Pattern)
在懒汉模式的基础上再改进一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Singleton { public: static Singleton* GetInstance() { if (singleton == NULL) { lock(); if (singleton == NULL) { singleton = new Singleton(); } unlock(); } return singleton; } static Singleton* singleton; }; Singleton* Singleton::singleton = NULL;
|
两次if判断避免了每次都要加锁。但是,这样仍是不安全的。因为”singleton = new Singleton();”这句不是原子的。
这句可以分为3步:
- 申请内存
- 调用构造函数
- 将内存指针赋值给singleton
上面这个顺序是我们期望的,可以编译器并不会保证这个执行顺序。所以也有可能是按下面这个顺序执行的:
- 申请内存
- 将内存指针赋值给singleton
- 调用构造函数
这样就会导致其他线程可能获取到未构造好的单例指针。
解决办法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Singleton { public: static Singleton* GetInstance() { if (singleton == NULL) { lock(); if (singleton == NULL) { Singleton* tmp = new Singleton(); memory_barrier(); singleton = tmp; } unlock(); } return singleton; } static Singleton* singleton; }; Singleton* Singleton::singleton = NULL;
|
语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。简单的说就是保证指令一定程度上的按顺序执行,避免上述所说的乱序行为。
把单例写成这么复杂也是醉了。
返回指针还是引用?
Singleton返回的实例的生存期是由Singleton本身所决定的,而不是用户代码。我们知道,指针和引用在语法上的最大区别就是指针可以为NULL,并可以通过delete运算符删除指针所指的实例,而引用则不可以。由该语法区别引申出的语义区别之一就是这些实例的生存期意义:通过引用所返回的实例,生存期由非用户代码管理,而通过指针返回的实例,其可能在某个时间点没有被创建,或是可以被删除的。但是这两条Singleton都不满足,所以返回引用更好一些。
结论
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Singleton { public: static Singleton& GetInstance() { static Singleton singleton; return singleton; } int Init(); private: Singleton(); Singleton(const Singleton&); Singleton& operator=(const Singleton&); };
|
这种写法比较简单,可以满足大多数场景的需求。如果不能满足需求,再考虑DCLP那种复杂的模式。如《UNIX编程艺术》中所说:“Keep it sample, Stupid!”
参考
The Singleton Pattern
面试中的Singleton
内存屏障