# 单例模式

单例模式是设计模式的一种,是开发实践中经常用到的一个概念。大意是在一个生命周期内(大多数情况是一个进程),只有一个实例。比如一个进程应该只有一个日志管理器 (LogManager) 实例。

这里给出一个单例模式的例子:

class Singleton {
   static Singleton *instance;
   int data;
 
   // Private constructor so that no objects can be created.
   Singleton() {
      data = 0;
   }
   public:
   static Singleton *getInstance() {
      if (!instance)
      instance = new Singleton;
      return instance;
   }
   int getData() {
      return this -> data;
   }
   void setData(int data) {
      this -> data = data;
   }
};
//Initialize pointer to zero so that it can be initialized in first call to getInstance
Singleton *Singleton::instance = 0;
int main(){
   Singleton *s = s->getInstance();
   cout << s->getData() << endl;
   s->setData(100);
   cout << s->getData() << endl;
   return 0;
}

其中 Singleton 类不能从外部实例化,只能调用 getInstance 静态方法生成 Singleton 的实例。

# 单例模式进阶

上面的例子可以精简一下:

class Singleton {
   static Singleton *instance;
   int data;
 
   Singleton() {
      data = 0;
   }
   //...
public:
   static Singleton *getInstance();
}

这里的 instance 静态成员变量不会被释放,导致可能存在泄漏。这里的泄漏大多是文件泄漏等,因为可能有未保存的文件内容等。

所以这里的 instance 可以使用智能指针:

#include <memory>
class Singleton {
   static std::unique_ptr<Singleton> instance;
   int data;
 
private:
   Singleton() {
      data = 0;
   }
   //...
public:
   static Singleton* getInstance();
}
// initialization of 'instance' variable
// ...
Singleton* Singleton::getInstance(){
    if (!instance)
        instance = std::unique_ptr<Singleton>(new Singleton); // you can use std::make_unique
    return instance.get();
}

由于这里能保证单例是肯定可以分配成功的,因为 new 大多数情况下不会失败,所以 getInstance 的返回类型可以改为:

static Singleton& getInstance();

然后,这个版本不是多线程安全的,因此需要加锁。但比较便捷的方式是如下:

#include <memory>
class Singleton {
   int data;
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator= (const Singleton) = delete;
private:
   Singleton() {
      data = 0;
   }
   //...
public:
   static Singleton& getInstance();
}
Singleton& Singleton::getInstance(){
	static Singleton instance;
    return instance;
}

在 c++11 中,静态局部变量的初始化是多线程安全的。

这里禁止了复制构造函数,因为单例类只需要一个实例。

# 单例模式模板进阶

每个类都写一个 getInstance 是比较麻烦的,我们可以使用模板来更方便的完成单例模式:

template<typename T>
class Singleton {
public:
    static T& instance();
    Singleton(const Singleton&) = delete;
    Singleton& operator= (const Singleton) = delete;
protected:
    struct token {}; //without needing to be a friend.
    Singleton() {}
};
#include <memory>
template<typename T>
T& Singleton<T>::instance()
{
    static T instance{token{}};
    return instance;
}

模板的详情可参考这个

# 单例模式引入的问题(针对传统的使用方式)

单例模式可以带来很多好处,因为我们不需要频繁的创建单例类的实例,在需要单例类的实例时,调用 getInstance 获取即可。

然而,单例模式也会带来一些问题,比如这个讨论这篇文章

如果空泛的解释这个问题,似乎有点没有头绪,那么接下来描述几个例子。

# Singletons are Pathological Liars

eg:Singletons are Pathological Liars

# 单例类之间的依赖引发崩溃

auto a = SingletonA::Intance();
a.func1();
// func1 implementation
SingletonA::func1(){
	//... some work
    single_b = SingletonB::Intance();
    single_b.func2();
}

如果 SingletonA 和 SingletonB 的 Instance 都是第一次被调用,那么 SingletonA 先被构造,之后是 SingletonB,在进程退出时,SingletonB 会先被析构,如果此时进程的某个线程调用了 a.func1,那么由于 SingletonB 的实例被析构了,所以可能出现访问异常,导致崩溃。

# 静态全局变量和单例类之间的依赖引发崩溃

auto a = SingletonA::Intance();
a.func1();
// func1 implementation
SingletonA::func1(){
	//... some work
    static classB b;
    b.func2();
}

该崩溃情况和上一小节的原理是一致的。

# 上述单例类的实现不是多线程安全的

在进程退出时,单例类的实例会被析构,如果此时有一个线程在使用这个单例类的实例,那么也可能会引发崩溃,但这种的情况概率一般比较低。

# 这些问题的原因

以上问题有三个点:

  • 依赖关系隐藏,导致新接手工程的开发者不清楚依赖关系。

  • 单例类在上述的实现方式中,是使用时初始化 (调用构造函数),那么多个单例类之间的初始化顺序是不可预测的,这会导致彼此的依赖不确定性。

  • 单例类在某种意义上,可以理解成是一个全局变量,全局变量不是多线程安全的,因此单例类也同样如此。

# 解决办法

  • 在进程的 main 方法中,显示的定义多个单例类之间的初始化顺序,保证在进程退出后,这些单例类的实例按照初始化的逆顺序进行析构。
  • 在需要的场景下,对单例类的析构函数和其他函数加锁。

之前想过在 main 方法的最后显示调用单例模式的析构函数,发现代码不知道怎么写。换一个角度思考,显示调用析构函数从设计上是不好的,另外,既然运行时做了单例类实例的初始化,那么相应的析构也应该由运行时来做。因此在 main 方法中调用析构函数是不合理的。

# 使用单例模式的原因

既然单例模式有上述问题,那为什么还要使用单例模式呢?

回到单例模式的初衷:保证一种资源在一个特定的环境下只有一个实例。

# 理想的解决办法

回顾单例模式的问题,可发现单例模式本身是没有问题的,错在单例模式的使用方法上。

那么如何更好的解决以上问题,以下是一个可行的办法:

  • 使用单例模式创建实例(保证唯一实例)

  • 给每个需要单例类实例的组件传一个单例类实例的参数(暴露依赖)

    • 这里和全局使用单例类实例的区别在于依赖被显式声明,同时依赖的顺序将会被固定,不存在不确定性。
    • 单元测试中,多线程情况下需要等待任务执行结束 (wait method),再退出单元测试。

在以上情况都不适用的情况下,可使用传统使用方式。

# 总结

单例模式有它的使用场景,其概念是好的,没有问题的。之所以被大家诟病的原因,其实是部分开发者对于单例模式的不当使用(因为全局状态 (global state) 的传递变得没有条理)。

另外,单例模式的使用中,需要注意单例类之间的初始化顺序,当然也要尽量避免单例类之间的依赖(不过这往往不太好避免)。

单例模式不易使用的场景是包含重要状态的一些类,这些类最好使用依赖传递。因为使用单例模式的话,会隐藏依赖关系,这样对于状态的改变就难以跟踪和分析。这时使用上一节提出的方法就会更好一点。

# 参考链接

  • Alternatives to the singleton pattern
  • What are drawbacks or disadvantages of singleton pattern?
  • Root Cause of Singletons
  • Modern C++ Singleton Template
  • Singleton Destructors
  • Patterns I Hate #1: Singleton