之前在看《Linux多线程服务端编程-使用muduo C++网络库》,看到 2.5 线程安全的Singleton实现 时,里面对单例模式的线程安全有这么一句话, 人们一直认为double checked locking是王道,有“神牛”指出由于乱序执行的影响,DCL(double checked locking)是靠不住的。 这对于没有经验的初学者有些难懂,所以我打算在这篇文章里,讲一讲,什么是单例,什么是double checked locking,double checked locking又为什么靠不住。
单例模式,简单来说就是保证一个类最多存在一个实例,并且这种保证是来自于设计者,而不是使用者。而实现这样需求的办法就是:让类的构造函数私有,在类内创建一个静态对象,并创建一个公有的静态方法访问这个对象。
可能会有人问, 为什么创建的对象必须是静态的? 因为我们在外面没办法创建这个对象,需要通过类来调用方法,而不是通过对象调用方法。要想通过类来调用方法,那么这个方法必须是static静态的(不需要this指针)。而静态的方法内只能调用类中的静态数据,所以,类内创建的对象必须是静态的。(下面可以看具体的代码)
单例模式的实现有懒汉模式和饿汉模式。下面是这两种模式的实现。
饿汉模式,就是在类定义的时候就实例化了(因为饿,主观能动性强 - . -)。
是线程安全的,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现(不用锁机制,开销小),可以实现更好的性能。
5 class Singleton{
6 private:
7 Singleton(){
8 cout<<"i am single"<<endl;
9 }
10
11 static Singleton* instance;
12 public:
13 static Singleton* getInstance(){
14 return instance;
15 }
16 };
17
18 Singleton* Singleton:: instance = new Singleton();
19
20 int main(){
21 Singleton* one = Singleton::getInstance();
22 Singleton* two = Singleton::getInstance();
23 if(one == two){
24 cout<<"确实是单例!"<<endl;
25 }
26 }
太懒了,等到第一次用的时候才去实例化。
在访问量较小时,采用懒汉实现。
class Singleton{
6 private:
7 Singleton(){
8 cout<<"i am single"<<endl;
9 }
10
11 static Singleton* instance;
12 public:
13 static Singleton* getInstance(){
14 if(instance == nullptr)
15 {
16 instance = new Singleton();
17 cout<< "it is first"<<endl;
18 }
19 else{
20 cout<<"it is not first"<<endl;
21 }
22 return instance;
23 }
24 };
25
26 Singleton* Singleton:: instance = nullptr;
27
28 int main(){
29 Singleton* one = Singleton::getInstance();
30 Singleton* two = Singleton::getInstance();
31 if(one == two){
32 cout<<"确实是单例!"<<endl;
33 }
34 }
饿汉模式是线程安全的,因为在类定义时就已经实例化了,而懒汉模式,在多线程时,很可能同时多个线程判断实例为null,然后都去创建新的实例。
5 class Singleton{
6 private:
7 Singleton(){
8 cout<<"i am single"<<endl;
9 pthread_mutex_init(&mutex,NULL);
10 }
11
12 static pthread_mutex_t mutex;
13 static Singleton* instance;
14 public:
15 static Singleton* getInstance(){
16 pthread_mutex_lock(&mutex);
17 if(instance == nullptr)
18 {
19 instance = new Singleton();
20
21 }
22
23 pthread_mutex_unlock(&mutex);
24 return instance;
25 }
26 };
27 pthread_mutex_t Singleton::mutex;
28 Singleton* Singleton:: instance = nullptr;
仅仅在判断是否为nullptr前,加了一个锁。但是这样效率会低,因为不管是否instance已经实例化,每次都要加锁进入临界区后才能做判断是否为nullptr。所以就有了下面的 double checked locking
class Singleton{
6 private:
7 Singleton(){
8 cout<<"i am single"<<endl;
9 pthread_mutex_init(&mutex,NULL);
10 }
11
12 static pthread_mutex_t mutex;
13 static Singleton* instance;
14 public:
15 static Singleton* getInstance(){
16 if(instance==nullptr){
17 pthread_mutex_lock(&mutex);
18 if(instance == nullptr)
19 {
20 instance = new Singleton();
21 }
22 pthread_mutex_unlock(&mutex);
23 }
24 return instance;
25 }
26 };
27 pthread_mutex_t Singleton::mutex;
28 Singleton* Singleton:: instance = nullptr;
在进入临界区之前先做判断,这样在已经有实例的情况下,就不会进入临界区,省去了加锁解锁。那为什么进入临界区还要做第二次判断呢?因为如果没有实例,假设这时候有两个线程A和B,都通过了第一个判断,其中线程A获得锁进入临界区,线程B阻塞在临界区外,那么线程A得到了一个实例化释放锁之后,线程B也会进入临界区内,又重新实例化一个对象。
对于
20 instance = new Singleton();
可以拆分成一下三步
Singleton对象分配空间。
在分配的空间中构造对象
使instance指向分配的空间
但是编译器并不是严格按照上面的顺序来执行的。可能交换2和3.
那么对于之前的DCL(double checked locking)
1 static Singleton* getInstance(){
2 if(instance==nullptr){
3 pthread_mutex_lock(&mutex);
4 if(instance == nullptr)
5 {
6 instance = new Singleton();
7 }
8 pthread_mutex_unlock(&mutex);
9 }
10 return instance;
11 }
可能出现这种情况:
这里仍然要注意的是局部变量初始化的线程安全性问题,在C++11以后,要求编译器保证静态变量初始化的线程安全性,可以不加锁。但C++ 11以前,仍需要加锁。
5 class Singleton{
6 private:
7 Singleton(){
8 pthread_mutex_init(&mutex,NULL);//c++11之后不需要
9 }
10
11 static pthread_mutex_t mutex;//c++11之后不需要
12 public:
13 static Singleton* getInstance();
14 };
15 pthread_mutex_t Singleton::mutex;//c++11之后不需要
16 Singleton* Singleton::getInstance(){
17 pthread_mutex_lock(&mutex);//c++11之后不需要
18 static Singleton instance;
19 pthread_mutex_unlock(&mutex);//c++11之后不需要
20 return &instance;
21 }
线程安全版本的单例确实涵盖面有些广,总之还是具体问题具体看待,不同的编译器,不同的c++标准都会有所影响,可能DCL(double checked locking)在以后的标准或编译器里又能做到线程安全。
本文内容如有不全面的地方,望多多指正。
以上