单例模式,懒汉饿汉,线程安全,double checked locking的问题

概览

  • 本文目的
  • 单例
    • 饿汉模式
    • 懒汉模式
  • 线程安全的Singleton实现
    • 懒汉普通加锁
    • double checked locking
      • double checked locking 靠不住?
      • 静态局部变量实现
  • 尾语


本文目的

  之前在看《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 }

输出结果:
在这里插入图片描述


线程安全的Singleton实现

 
  饿汉模式是线程安全的,因为在类定义时就已经实例化了,而懒汉模式,在多线程时,很可能同时多个线程判断实例为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
 

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也会进入临界区内,又重新实例化一个对象。
 

double checked locking 靠不住?

 
对于

20	instance = new Singleton();

 
可以拆分成一下三步

  1. Singleton对象分配空间。

  2. 在分配的空间中构造对象

  3. 使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         }   

可能出现这种情况:

  1. 线程A进入了getInstance函数,并且执行了step1和step3,然后挂起。这时的状态是:instance不为nullptr,但instance指向的内存去没有对象!
  2. 线程B进入了getInstance函数,发现instance不为nullptr,就直接return instance。
     

静态局部变量实现

 
这里仍然要注意的是局部变量初始化的线程安全性问题,在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++11之前 局部静态变量初始化的线程不安全可参考: C++局部静态初始化不是线程安全!
  • c++11 静态变量初始化的线程安全性参考:C++11中静态局部变量初始化的线程安全性

尾语

  线程安全版本的单例确实涵盖面有些广,总之还是具体问题具体看待,不同的编译器,不同的c++标准都会有所影响,可能DCL(double checked locking)在以后的标准或编译器里又能做到线程安全。

  本文内容如有不全面的地方,望多多指正。

以上

你可能感兴趣的:(muduo)