C++ 单例模式

单例模式指的是在一个进程中,只允许存在一个类的实例。在C++中,通过将其构造函数定义为private,然后提供一个getInstance()的接口来实现。这个接口通过一些存在性判断,控制只有一个实例产生(如果没有则创建,如果有则返回之前的)。

1. 一个简单的单例模式

  7 class Singleton {
  8   private:    
  9     static Singleton* instance;    
 10     Singleton() {} // 构造函数私有化,防止实例化多个对象
 11   public:    
 12     static Singleton* getInstance() {        
 13       if(instance == nullptr) {            
 14         instance = new Singleton();        
 15       }        
 16       return instance;    
 17     }    
 18     void someMethod() {        // 该单例类的一些方法    
 19     }
 20 };
 21 
 22 Singleton* Singleton::instance = nullptr; // 静态成员变量需要在类外初始化

2. 饿汉式与懒汉式

饿汉式与懒汉式指的是创建对象的时机,饿汉式指在程序开始运行时便“迫不及待”创建对象,而懒汉式指的是,只有某个地方调用了getInstance()后,才“慢悠悠”的创建对象。

所以,饿汉式在类外实例化对象,而懒汉式则在getInstance()中实例化对象,上面的例子显然属于懒汉式,对应的饿汉式代码如下:

  7 class Singleton {
  8   private:    
  9     static Singleton* instance;    
 10     Singleton() {} // 构造函数私有化,防止实例化多个对象
 11   public:    
 12     static Singleton* getInstance() {           
 16       return instance;    
 17     }    
 18     void someMethod() {        // 该单例类的一些方法    
 19     }
 20 };
 21 
 22 Singleton* Singleton::instance = new Singleton(); // 静态成员变量需要在类外初始化

3. 指针与引用

上面的两个例子getInstance()都是返回对象的指针,其实也可以返回对象的引用。此时,成员变量不再是指针,而直接是一个对象。

  7 class Singleton {
  8   private:    
 10     Singleton() {} // 构造函数私有化,防止实例化多个对象
 11   public:    
 12     static Singleton& getInstance() {
            static Singleton instance;            
 16       return instance;    
 17     }    
 18     void someMethod() {        // 该单例类的一些方法    
 19     }
 20 };
  7 class Singleton {
  8   private:    
  9     static Singleton instance;    
 10     Singleton() {} // 构造函数私有化,防止实例化多个对象
 11   public:    
 12     static Singleton& getInstance() {           
 16       return instance;    
 17     }    
 18     void someMethod() {        // 该单例类的一些方法    
 19     }
 20 };
 21 
 22 Singleton Singleton::instance; // 静态成员变量需要在类外初始化

4. 线程安全

现在我们有了4种模式,它们创建的线程安全性如下:

饿汉式 懒汉式
返回指针 线程安全 线程不安全
返回引用 线程安全 线程安全

饿汉式都是线程安全的,因为它创建对象在程序初始化全局变量时,此时还没有多线程存在。

懒汉式的引用版本是线程安全的,指针版本不是线程安全的,这里面有一段历史故事:

其实懒汉式由于将创建对象推迟到了getInstance()接口中,如果没有同步机制,很难保证是否会有两个线程同时调用它,所以懒汉式天生是线程不安全的。但在c++11中作了如下规定:编译器要保证静态局部变量构造的线程安全性。那编译器是怎么做到这一点的呢,其实非常简单,那就是加锁(如果你理解操作系统,你就发现这个世界上不存在任何所谓新的语言特性)。不过这会导致一些问题,后面我们再说。

这样一来,懒汉式就借了静态局部变量的东风,“不用加锁”就能保证线程安全了。指针则没有这种便利了,必须手动加锁以保证线程安全性。

基于以上事实,很多人得到了“利用静态局部变量来实现单例模式是最佳实践”的结论,当然这并没有什么过错,但正如刚提到的,静态局部变量是默认加锁的!所以哪怕是单线程中,只要你使用了静态局部变量,那就会有加锁解锁的开销(所以这也是静态局部变量的一个天生缺陷)。反观饿汉式则天生线程安全,并不需要加锁。

还有一个容易让人忽略的地方是,虽然我们讨论了这么多线程安全性,但这仅仅是限于构造这个对象时的,并不包括对这对象的访问

所以无论是饿汉式还是懒汉式,是指针版本还是引用版本,如果要保证线程安全,所有访问对象成员的操作都需要手动加锁

5. 最佳实践

真香定律告警!!!!!

虽然说了这么多懒汉式的缺点,但毕竟使用方便,锁的开销其实也没那么大,所以如果要实现一个单例模式,懒汉式还是首选。(还有一部分原因是,单例模式大部分情况是需要线程安全的,所以就算采用饿汉式,后面访问对象还是需要手动加锁)

而指针还需要自己new 还要自己想着delete,所以首选引用模式。

再考虑到线程安全性,需要手动加锁保护成员变量:

 81 class Singleton {
 82   private:
 83     Singleton() = default;
 84     std::mutex mtx; //要保证线程安全,必须加锁
 85   public:
 86     static Singleton& getInstance() {
 87       static Singleton instance;
 88       return instance;
 89     }
 90     virtual ~Singleton() noexcept = default; //生成析构函数
 91 
 92     Singleton(Singleton const &other) = delete; //禁用复制构造函数
 93     Singleton(Singleton &&other) = delete; //禁用移动构造函数
 94 
 95     Singleton &operator=(Singleton const &other) & = delete; //禁用赋值操作符
 96     Singleton &operator=(Singleton &&other) & = delete; //禁用移动操作符
 97     void someMethod() {        // 该单例类的一些方法
 98       std::unique_lock lock(mtx)
 99     }
100 };

你可能感兴趣的:(单例模式,c++,java)