0%

设计模式之单例模式(Singleton)

一、动机

在软件系统中,经常有这样一种特殊的类,必须保障它们在系统内中存在一个实例,才能确保它们的逻辑正确性,以及良好的效率。如何绕过常规的构建器,提供一个机制来保证一个类只有一个实例。比如Windows的任务管理器就是很典型的例子,你不能同时打开两个不同的任务管理器。

二、解决方案

单例模式是一种创建型设计模式,让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。单例模式可以:

  • 保证一个类只有一个实例。

  • 为该实例提供一个全局访问节点。

它的运作方式是这样的:如果你创建了一个对象,同时过一会儿后你决定再创建一个新对象,此时你会获得之前已创建的对象,而不是一个新对象。注意,普通构造函数无法实现上述行为,因为构造函数的设计决定了它必须总是返回一个新对象。Singleton模式中的实例构造器可以设置为protected以允许子类派生。如何实现多线程环境下安全的Singleton?注意对双检查锁的正确实现。

所有单例的实现都包含以下两个相同的步骤:

  • 将默认构造函数设为私有,防止其他对象使用单例类的new运算符。
  • 新建一个静态构建方法作为构造函数。该函数会“偷偷”调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。

如果你的代码能够访问单例类,那它就能调用单例类的静态方法。无论何时调用该方法,它总是会返回相同的对象。

三、模板模式的结构

structure_of_Singleton

单例Singleton类声明了一个名为get­Instance获取实例的静态方法来返回其所属类的一个相同实例。单例的构造函数必须对客户端Client代码隐藏。 调用获取实例方法必须是获取单例对象的唯一方式。

四、Singleton代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <thread>

// 线程安全版本单例实现
// 使用 static local 变量既能保证线程安全性,又能保证对象唯一
class Singleton {
private:
int _value;
// 将实际使用的构造函数设置为私有
Singleton(int value) : _value(value) {}
~Singleton() {}

public:
// 将默认构造函数,拷贝构造函数和拷贝赋值运算符都 delete 掉或者设置为私有
Singleton() = delete;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

// 只提供一个静态的获取实例的方法,用来调用
// 内部调用 protected 区的唯一一个构造函数来实例化对象
static Singleton *GetInstance(int value) {
static Singleton instance(value);
return &instance;
}

int value() const { return _value; }
};

void ThreadFoo() {
// Following code emulates slow initialization.
std::this_thread::sleep_for(std::chrono::milliseconds(500));
Singleton *singleton = Singleton::GetInstance(12);
std::cout << singleton->value() << std::endl;
}

void ThreadBar() {
// Following code emulates slow initialization.
std::this_thread::sleep_for(std::chrono::milliseconds(500));
Singleton *singleton = Singleton::GetInstance(23);
std::cout << singleton->value() << std::endl;
}

int main() {
std::cout << "If you see the same value, then singleton was reused (yay!\n"
<< "If you see different values, then 2 singletons were created (booo!!)\n"
<< "RESULT:\n";
std::thread t1(ThreadFoo);
std::thread t2(ThreadBar);
t1.join();
t2.join();
}

Execution result:
If you see the same value, then singleton was reused (yay!
If you see different values, then 2 singletons were created (booo!!)
RESULT:
12
12
线程安全实现之双检查锁

因为存在内存读写乱序(编译器问题),实则不安全。

原因分析:m_instance = new Singleton()这句话可以分成三个步骤来执行:

  • 分配了一个Singleton类型对象所需要的内存;
  • 在分配的内存处构造Singleton类型的对象;
  • 把分配的内存的地址赋给指针m_instance

一般会认为这三个步骤是按顺序执行的,但实际上只能确定步骤1是最先执行的,步骤23却不一定。问题就出现在这。

假如某个线程A在调用执行m_instance = new Singleton()的时候是按照1,3,2的顺序的,那么刚刚执行完步骤3Singleton类型分配了内存(此时m_instance就不是nullptr了)就切换到了线程B,由于m_instance已经不是nullptr了,所以线程B会直接执行return m_instance得到一个对象,而这个对象并没有真正的被构造!严重bug就这么发生了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <thread>
#include <mutex>

class Singleton {
private:
int _value;
static Singleton *_instance;
static std::mutex _mutex;
Singleton(const std::string value) : m_value(value) {}
~Singleton() {}

public:
Singleton() = delete;
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;

// 双检查锁版本
static Singleton *GetInstance(int value) {
if(m_instance == nullptr) {
std::unique_lock<std::mutex> lock(_mutex);
if(_instance == nullptr) {
_instance = new Singleton(value);
}
}
return _instance;
}

int value() const { return _value; }
};

Singleton *Singleton::_instance{nullptr};
std::mutex Singleton::_mutex{};
五、使用场景
  • 如果程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式。

单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。该方法可以创建一个新对象,但如果该对象已经被创建,则返回已有的对象。

  • 如果你需要更加严格地控制全局变量,可以使用单例模式。

单例模式与全局变量不同,它保证类只存在一个实例。除了单例类自己以外,无法通过任何方式替换缓存的实例。

请注意,你可以随时调整限制并设定生成单例实例的数量,只需修改获取实例方法,即 GetInstance中的代码即可实现。

六、优缺点
  • 你可以保证一个类只有一个实例。
  • 你获得了一个指向该实例的全局访问节点。
  • 仅在首次请求单例对象时对其进行初始化。
  • 违反了_单一职责原则_。该模式同时解决了两个问题。
  • 单例模式可能掩盖不良设计,比如程序各组件之间相互了解过多等。
  • 该模式在多线程环境下需要进行特殊处理,避免多个线程多次创建单例对象。
  • 单例的客户端代码单元测试可能会比较困难,因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的,而且绝大部分语言无法重写静态方法,所以你需要想出仔细考虑模拟单例的方法。要么干脆不编写测试代码,或者不使用单例模式。
七、与其它模式的关系
  • 外观模式类通常可以转换为单例模式类,因为在大部分情况下一个外观对象就足够了。
  • 如果你能将对象的所有共享状态简化为一个享元对象,那么享元模式就和单例类似了。但这两个模式有两个根本性的不同。
    1. 只会有一个单例实体,但是享元类可以有多个实体,各实体的内在状态也可以不同。
    2. 单例对象可以是可变的。享元对象是不可变的。
  • 抽象工厂模式生成器模式原型模式都可以用单例来实现。