ZyLiu's Blog

回顾单例模式

字数统计: 1.5k阅读时长: 6 min
2020/09/12 Share

C++岗位面试中,面试官常常会问道,你常用哪些设计模式。我通常会回答:单例模式、策略模式、工厂模式、模板方法模式等。
其中单例模式实现方法比较固定,面试官有时会让我手写单例模式。为了在面试中能够准确无误的写出单例模式,本文对单例模式的C++实现进行回顾与总结。

单例模式的定义

让类负责保存其自己的实例,提供一个方法(全局访问点)获取该实例,并且不能从外部创建该类的其他实例,确保该类只有一个实例。

  • 为什么要使用单例模式,使用全局静态变量不可以吗?
    全局变量无法保证只有一个实例,因此单例模式有其存在的必要。

单例模式的实现方式

单例模式通常有两种实现方式,分为“懒汉式实现”“饿汉式实现”。它们的区别在于创建实例的时机。

  • 懒汉式实现:只有需要获取实例时才创建实例。适用于不确定是否需要访问实例的情形,且实例访问非常少。
  • 饿汉式实现:程序运行后立即创建实例。适用于需要频繁访问getInst的情形。相当于用空间换时间。

饿汉模式的单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton {
public:
static Singleton* getInst() {
return &m_inst;
}
private:
Singleton();
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

private:
static Singleton m_inst;
};

Singleton Singleton::m_inst;

懒汉模式的单例模式

非线程安全实现

这是《设计模式》一书中的实现方式,是一种线程不安全的实现方式。

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
class Singleton {
public:
static Singleton* getInst() {
if (!m_inst) {
m_inst = new Singleton();
}
return m_inst;
}
private:
Singleton();
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

// 一个用于释放实例的内部类
class Garbo {
public:
Garbo() {

}
~Garbo() {
if (m_inst) {
delete m_inst;
}
}
};
private:
static Singleton *m_inst;
};

static Singleton *Singleton::m_inst = nullptr;
static Singleton::Garbo Singleton::m_grabo;

上述代码的Garbo类使代码十分臃肿,考虑用智能指针替代:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Singleton {
public:
static Singleton* getInst() {
if (!m_inst) {
m_inst = make_shared<Singleton>();
}
return m_inst.get();
}
private:
Singleton();
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

private:
static shared_ptr<Singleton> m_inst;
};

static shared_ptr<Singleton> Singleton::m_inst;

双重检查实现

上述实现中,没有考虑多线程访问的情形。
下面先实现一个虽然加锁保护,但是效率极低的版本。
该版本中每次调用getInst都要访问互斥锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Singleton {
public:
static Singleton* getInst() {
lock_guard(m_mutexInst);
if (!m_inst) {
m_inst = make_shared<Singleton>();
}
return m_inst.get();
}
private:
Singleton();
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

private:
static shared_ptr<Singleton> m_inst;
static mutex m_mutexInst;
};

static shared_ptr<Singleton> Singleton::m_inst;
static mutex Singleton::m_mutexInst;

如果先进行实例判断,再进入互斥锁,是否可行呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Singleton {
public:
static Singleton* getInst() {
if (!m_inst) {
lock_guard(m_mutexInst);
m_inst = make_shared<Singleton>();
}
return m_inst.get();
}
private:
Singleton();
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

private:
static shared_ptr<Singleton> m_inst;
static mutex m_mutexInst;
};

static shared_ptr<Singleton> Singleton::m_inst;
static mutex Singleton::m_mutexInst;

思考一下两个线程同时进入if语句的情况,会发现实例被创建了两次,此时引起了内存泄漏,因此该实现是错误的。

下面是双重检查的实现:

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 {
public:
static Singleton* getInst() {
if (!m_inst) {
lock_guard(m_mutexInst);
if (!m_inst) {
m_inst = make_shared<Singleton>();
}
}
return m_inst.get();
}
private:
Singleton();
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

private:
static shared_ptr<Singleton> m_inst;
static mutex m_mutexInst;
};

static shared_ptr<Singleton> Singleton::m_inst;
static mutex Singleton::m_mutexInst;

可以看出这个实现非常臃肿,而且它也并非完美。
根据《程序员的自我修养–链接、装载与库》一书,p29上的描述,CPU的乱序执行会导致一些问题。
主要是由于new的三个阶段也并非是原子的:

  1. 申请内存
  2. 在该内存上调用构造函数
  3. 将内存地址反回

问题在于二三步是可以颠倒的。会出现以下情况:
线程A中的m_inst = new Singleton()已执行第三步,却未执行第二步,此时调度到线程B,判断m_inst为非空,并返回该指针,此时m_inst上的操作可能引起崩溃问题。

局部静态对象实现

下面是被称为Meyers' Singleton的单例实现,使用一个局部的static变量保存单例实例。

1
2
3
4
5
6
7
8
9
10
11
class Singleton {
public:
Singleton& getInst() {
static Singleton inst;
return inst;
}
private:
Singleton();
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};

该实现是由著名的写出《Effective C++》系列书籍的作者Meyers提出的。所用到的特性是在C++11标准中的Magic Static特性:

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。

这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。

C++静态变量的生存期 是从声明到程序结束,这也是一种懒汉式。

这是最推荐的一种单例实现方式:

  1. 通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3, VS2015支持该特性);
  2. 不需要使用共享指针,代码简洁;
  3. 注意在使用的时候需要声明单例的引用 Single& 才能获取对象。

如果想在C++11标准之前使用局部静态变量方式的单例模式,可以怎么做呢?恐怕必须要加锁保护静态变量的初始化了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Singleton {
public:
Singleton& getInst() {
mutex.lock()
static Singleton inst;
mutex.unlock();
return inst;
}
private:
Singleton();
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

private:
// 不要在意这些细节
static mutex m_mutexInst;
};

static mutex Singleton::m_mutexInst;

参考文献:

C++ 单例模式总结与剖析

CATALOG
  1. 1. 单例模式的定义
  2. 2. 单例模式的实现方式
  3. 3. 饿汉模式的单例模式
  4. 4. 懒汉模式的单例模式
    1. 4.1. 非线程安全实现
    2. 4.2. 双重检查实现
    3. 4.3. 局部静态对象实现