单例模式(Singleton Pattern)

定义:

​ 该设计模式属于创建型模式, 其意图是保证一个类仅有一个实例, 并提供一个可以访问它的全局访问点, 保证所有的程序都可以访问该类所提供的唯一 一个实例.

对于概念的部分, 本文并不过多叙述, 主要还是侧重于代码层面的问题

结构

Hywy4O.png

如果要构建一个单例模式:

  1. 要使其构造函数私有化, 保证不会在类外被其他程序创建
  2. 使用静态变量或者静态指针定义或指向该唯一对象
  3. 使用一个 public 静态方法来获取该对象

最简单的懒汉型单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton 
{
private:
//****数据****
public:
static Singleton* p;
static Singleton* getInstance() {
if (p == nullptr) //1
p = new Singleton(); //2
return p; //3
}

private:
Singleton() {};
~Singleton() { };
};

​ 如果刚开始看起来这种型式的单例模式, 可能会认为这种写法是比较正确的. 事实上, 如果在只考虑单线程的情况下, 这种代码模式是正确的, 可是当在多线程的环境下, 当线程 Ⅰ 进入步骤 1 但是还没有执行步骤 2 时, 时间片耗尽, 此时 p 依旧是一个 nullptr 此时当线程 Ⅱ 在步骤 1 时发现, p 为空指针, 所以它也会进入步骤 2, 这就导致了该对象被多次创建, 这就违反了单例模式的设计原则, 并且会导致第一次创建对象的地址丢失, 导致内存泄漏, 于是考虑出加锁的形式来限制

线程安全模式

1
2
3
4
5
6
7
8
//线程安全版本,但锁的代价过高
Singleton* Singleton::getInstance() {
Lock lock;
if (p == nullptr) {
p = new Singleton();
}
return p;
}

双检查锁机制

这个版本是线程安全的, 但是对于单例模式, 读操作是可以同时发生的, 只有写操作是需要保证只有一次操作的, 但是这个版本的代码则无论是否是读操作或是写操作, 都要进行加锁, 这样就导致了加锁的性能消耗过大

所以对于根据读写操作进行加锁的思想, 引出了 双检查锁机制

1
2
3
4
5
6
7
8
9
10
11
//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {

if(p==nullptr){
Lock lock;
if (p == nullptr) {
p = new Singleton();
}
}
return p;
}

但是对于双检查锁机制, 可能会有这样的疑问, 既然已经检查过 p 是否为空了, 为什么还要检查一次呢? 不妨模拟一下多线程环境下对于这段代码的访问顺序, 不难发现, 虽然在 p 为空时进行了加锁, 但是依旧可以有多个线程同时进入第 4 行, 既然进入了第 4 行, 如果不再进行检查, 即使加锁, 不同的线程也会在拿到锁之后执行完第一个 if(p==nullptr) 中的代码, 依旧会出现该对象被多次创建的问题, 所以双检查锁机制是必要的

C++ 11版本之后的跨平台实现 (volatile)

在使用了双检查锁机制后, 对于单例模式的实现似乎就已经无懈可击了, 不过在后来的使用中, 这种实现则被发现并不是绝对正确的. 问题的来源来自于编译器的优化方式, 对于不同版本的编译器, 其在底层创建对象的时候, 可能并不是先构造对象内存空间, 然后调用构造函数, 而是可能以某一种顺序执行, 这就带来了问题.

由于构造对象的过程并不是原子操作, 所以当线程第一次创建对象时, 时间片耗尽, 另一线程要去获取该对象, 由于此时 p 已经不为空, 则将会把一个没有进行初始化的对象返回, 从而导致错误, 为了解决这个问题, c++ 11 中采用了原子操作的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//C++ 11版本之后的跨平台实现 (volatile)
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);//释放内存fence
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}

上面的方式实现又太过复杂, 由于 c++11 之后在创建对象时强制将其设置为原子操作, 故直接使用下面的简略写法即可

1
2
3
4
Singleton Singleton::getInstance() {
static Singleton singleton;
return singleton;
}