单例模式指的是在一个进程中,只允许存在一个类的实例。在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 };