0%

【设计模式】单例模式

本篇介绍了单例模式及相关的面向对象设计原则。单例模式保证一个类仅有一个实例,并提供一个该实例的全局访问点。重点在于如何实现线程安全的单例模式。

1 经典单例模式

许多时候,我们程序中的某些对象只需要一个或者只能存在一个,比如线程池、缓存、硬件的驱动等,如果存在多个实例就会造成逻辑错误或程序异常。因此作为类的设计者,就需要保证这个类在程序中只能存在一个实例对象。

显然,利用全局静态变量似乎就可以做到,但是全局静态变量并不完美。比如我们需要唯一的一个全局静态对象,那么这个全局对象在程序一开始就会被分配内存,如果后续并没有使用到这个全局对象,就造成了资源的浪费。而使用单例模式,可以随时在需要时才创建这个唯一的对象,并保证不会出现第二个对象。

经典的单例模式实现非常简单,在类中设定一个该类的静态对象成员,将构造函数设置为 private,然后提供一个静态的 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
class Singleton {
private:
static Singleton* uniqueInstance;
// 防止外部构造、析构、拷贝和移动
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton &operator=(const Singleton&) = delete;
Singleton &operator=(Singleton&&) = delete;
public:
// 全局的对象访问接口
static Singleton* getInstance() {
if(uniqueInstance == nullptr) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
// 全局的对象删除接口
static void deleteInstance() {
if(uniqueInstance) {
delete uniqueInstance;
uniqueInstance = nullptr;
}
}
};

// 静态对象初始化
static Singleton* Singleton::uniqueInstance = nullptr;

2 线程安全的单例模式

上面的经典实现在多线程的情况下会出现问题,有可能创建出两个实例对象,例如:

image-20230304162841491

因此上述实现不是线程安全的。要实现线程安全的单例模式有多种方法。

2.1 DCLP

一种经典的方法是 Double-Checked Locking Pattern (DCLP),在构造对象时使用互斥锁:

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
class Singleton {
private:
static Singleton* uniqueInstance;
static std::mutex m_mutex; //互斥锁
// 防止外部构造、析构和拷贝
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton &) = delete;
Singleton(Singleton &&) = delete;
Singleton &operator=(const Singleton &) = delete;
Singleton &operator=(Singleton &&) = delete;
public:
// 全局的对象访问接口
static Singleton* getInstance() {
// DCLP
if(uniqueInstance == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
if(uniqueInstance == nullptr) {
uniqueInstance = new Singleton();
}
}
return uniqueInstance;
}
// 全局的对象删除接口
static void deleteInstance() {
if(uniqueInstance) {
delete uniqueInstance;
uniqueInstance = nullptr;
}
}
};

// 静态对象初始化
static Singleton* Singleton::uniqueInstance = nullptr;
static std::mutex Singleton::m_mutex;

但 DCLP 也不是绝对安全的,问题出在uniqueInstance = new Singleton()这一句上,我们知道 C++ 中 new 一个对象实际上进行了三次操作:

  • 首先调用 operator new 申请一块内存
  • 然后调用构造函数在该内存上构造对象
  • 最后将该内存地址赋值给指针

但由于编译器优化的原因,C++ 编译器可能会在构造函数不抛出异常的前提下调整语句的执行顺序,也就是上面的三步可能变成:

  • 首先调用 operator new 申请一块内存
  • 然后将该内存地址赋值给指针
  • 最后调用构造函数在该内存上构造对象

在单线程的情况下,这样的顺序变化不会影响程序的结果,但在多线程情况下就不一定了,比如:

image-20230304164102931

这种情况下,线程 A 恰好完成内存申请,并且将内存地址赋给指针,但是还没有调用构造函数,此时线程 B 执行到指针判断,判断指针不为空,于是返回该对象指针,然后调用该对象的函数,但是这时该对象还没有进行构造,就产生了错误。

DCLP 产生该问题的关键在于 new 的操作不是原子的,虽然使用互斥锁对指针的写操作uniqueInstance = new Singleton()加了锁,但没有对之前的读操作if(uniqueInstance == nullptr) 加锁,从而产生了线程不安全问题。

因此一种解决方案是利用原子变量std::atomic将对象指针变为原子类型,并使用std::memory_order_acquirestd::memory_order_release强制对该指针的内存操作顺序不变,从而避免线程不安全:

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
class Singleton {
private:
static std::atomic<Singleton*> uniqueInstance;
static std::mutex m_mutex; //互斥锁
// 防止外部构造、析构和拷贝
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton &) = delete;
Singleton(Singleton &&) = delete;
Singleton &operator=(const Singleton &) = delete;
Singleton &operator=(Singleton &&) = delete;
public:
// 全局的对象访问接口
static Singleton* getInstance() {
// 保证内存命令的顺序不会被重排
Singleton* ins = uniqueInstance.load(std::memory_order_acquire);
if(ins == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
ins = uniqueInstance.load(std::memory_order_acquire);
if(ins == nullptr) {
ins = new Singleton();
uniqueInstance.store(ins, std::memory_order_release);
}
}
return uniqueInstance;
}
// 全局的对象删除接口
static void deleteInstance() {
if(uniqueInstance) {
delete uniqueInstance;
uniqueInstance = nullptr;
}
}
};

// 静态对象初始化
static std::atomic<Singleton*> Singleton::uniqueInstance = nullptr;
static std::mutex Singleton::m_mutex;

2.2 call_once

除了 DCLP 之外,还有一种更简单的实现线程安全的单例模式的方法,利用 C++ 的 std::call_once() 来实现,std::call_once() 可以保证函数只被线程安全的调用一次:

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
class Singleton {
private:
static Singleton* uniqueInstance;
static std::once_flag m_flag; // 函数调用标识
// 防止外部构造、析构和拷贝
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton &) = delete;
Singleton(Singleton &&) = delete;
Singleton &operator=(const Singleton &) = delete;
Singleton &operator=(Singleton &&) = delete;
// 创建对象
static void createInstance() {
uniqueInstance = new Singleton();
}
public:
// 全局的对象访问接口
static Singleton* getInstance() {
std::call_once(m_flag, createInstance);
return uniqueInstance;
}
// 全局的对象删除接口
static void deleteInstance() {
if(uniqueInstance) {
delete uniqueInstance;
uniqueInstance = nullptr;
}
}
};

// 静态对象初始化
static Singleton* Singleton::uniqueInstance = nullptr;
static std::once_flag Singleton::m_flag;

2.3 Meyers 单例模型

最后还有一种最为简单的实现,利用 C++ 的局部静态变量机制,C++ 的局部静态变量在第一次被调用时进行初始化,且保证只初始化一次,因此可以直接在 getInstance() 方法中初始化一个局部静态对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Singleton {
private:
// 防止外部构造、析构和拷贝
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton &) = delete;
Singleton(Singleton &&) = delete;
Singleton &operator=(const Singleton &) = delete;
Singleton &operator=(Singleton &&) = delete;
public:
// 全局的对象访问接口
static Singleton* getInstance() {
static Singleton* uniqueInstance;
return uniqueInstance;
}
// 全局的对象删除接口
static void deleteInstance() {
if(uniqueInstance) {
delete uniqueInstance;
uniqueInstance = nullptr;
}
}
};
---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。