设计模式(二):单例模式
单例模式
保证一个类仅提供一个实例,并且通过此唯一实例提供类数据的全局访问,该模式被称为单例模式,单例模式常常应用于仅需单一对象的任务,例如打印时仅有一个打印上下文对象、写入文件时仅提供一个对象防止繁琐的同步机制等。
为了实现这种特性,单例模式的类采用了:
私有化的构造函数:禁止在类外实例化类对象;
禁用的拷贝、赋值构造函数:防止出现第二个实例;
单例模式的另一个初衷是避免单一实例反复构造和析构带来大量的开销,且通过类名即可访问,因此单例模式采用了静态成员变量对象,为了操作这个静态成员变量提供了公共的静态成员函数接口,通过此函数向类外提供静态对象。
一个单例模式的类如下: 1
2
3
4
5
6
7
8
9
10class Singleton{
public:
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
static Singleton* getInstance();
private:
Singleton() = default;
static Singleton* sObj;
};
饿汉模式
单例模式的实现分成饿汉模式和懒汉模式,区别是何时初始化该静态对象。对于饿汉模式,在类初始化时已经初始化了该静态对象,因此每次类外通过静态函数获取该对象时,总能如愿获取该对象,仿佛饥饿的人总是提前先准备好食物,无论是否使用;而对于懒汉模式,则不会提前初始化该对象。
饿汉模式的实现:静态成员直接进行new初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using namespace std;
class Singleton{
public:
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
static Singleton* getInstance(){
return sObj;
}
private:
Singleton() = default;
static Singleton* sObj;
};
Singleton* Singleton::sObj = new Singleton(); //C++基础:静态成员变量需要类外初始化
int main(){
Singleton* obj = Singleton::getInstance();
return 0;
}
可见饿汉模式下,无论getInstance
是否被类外调用,对象总会在初始化时被创建,因此适用于那种创建代价较小、对象几乎一定被使用的场景。
懒汉模式
懒汉模式则没有这种未雨绸缪的计划,只有getInstance
时,才会初始化静态对象,如下:
1 | class Singleton{ |
这段代码在多线程下显然是不安全的,完全可能在某个时刻多个线程同时检查到sObj``为nullptr
,导致多个实例常见,违背了单例模式的规则。
因此需要考虑线程安全问题,C++常用三种方式去确保这个线程的安全。
懒汉模式的线程安全
双判断+互斥锁方法
这里双判断的作用:假如去除内层判断,压根没有对检查加锁,仅保证new过程是原子的,没什么卵用;假如去除外层判断,那么调用getInstance
时都需要加锁、解锁开销,因此这里需要使用双判断的方法。
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
using namespace std;
class Singleton{
public:
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
static Singleton* getInstance(){
if(sObj == nullptr){
slock.lock();
if(sObj==nullptr)
sObj = new Singleton();
slock.unlock();
}
return sObj;
}
private:
Singleton() = default;
static Singleton* sObj;
static mutex slock;
};
Singleton* Singleton::sObj = nullptr; //C++基础:静态成员变量需要类外初始化
mutex Singleton::slock;
int main(){
Singleton* obj = Singleton::getInstance();
return 0;
}
sObj
不为nullptr
需要经过三个步骤:
构造sObj指针;
new Singleton()
为类对象开辟并初始化堆区内存;将
sObj
指针指向该初始化内存。
然而重排序后可能指令成为了1-3-2,在3结束后时间片挂起该线程,其他线程会拿到这个对象,是未被new初始化的对象,因此和操作野指针一样,导致未定义行为。
因此另一种方法是引入原子变量来实现懒汉模式。
原子变量方法
假设该类是一个平凡可复制类型(作为原子变量的基本要求,详见C++ 11 新特性总结(三):多线程编程),只要将该静态对象原子化,那么一定能保证其他线程获取该对象时,是已经初始化的内存对象,如下:
1 |
|
然而对原子化类对象进行操作性能开销是比较大的,甚至在很多应用场合人们宁愿忍受指令重排序带来的race condition,也不愿意采用原子变量的方法,因此也产生第三种方法。C++ 11以后编译器保证了返回静态局部对象的线程安全,表现在该对象仅被初始化一次,且这个初始化过程是线程安全的。
静态局部对象方法
1 |
|
参考链接: