单例模式
单例模式(Singleton Pattern)
定义:
该设计模式属于创建型模式, 其意图是保证一个类仅有一个实例, 并提供一个可以访问它的全局访问点, 保证所有的程序都可以访问该类所提供的唯一 一个实例.
对于概念的部分, 本文并不过多叙述, 主要还是侧重于代码层面的问题
结构
如果要构建一个单例模式:
- 要使其构造函数私有化, 保证不会在类外被其他程序创建
- 使用静态变量或者静态指针定义或指向该唯一对象
- 使用一个 public 静态方法来获取该对象
最简单的懒汉型单例模式
1 | class Singleton |
如果刚开始看起来这种型式的单例模式, 可能会认为这种写法是比较正确的. 事实上, 如果在只考虑单线程的情况下, 这种代码模式是正确的, 可是当在多线程的环境下, 当线程 Ⅰ 进入步骤 1 但是还没有执行步骤 2 时, 时间片耗尽, 此时 p 依旧是一个 nullptr
此时当线程 Ⅱ 在步骤 1 时发现, p 为空指针, 所以它也会进入步骤 2, 这就导致了该对象被多次创建, 这就违反了单例模式的设计原则, 并且会导致第一次创建对象的地址丢失, 导致内存泄漏, 于是考虑出加锁的形式来限制
线程安全模式
1 | //线程安全版本,但锁的代价过高 |
双检查锁机制
这个版本是线程安全的, 但是对于单例模式, 读操作是可以同时发生的, 只有写操作是需要保证只有一次操作的, 但是这个版本的代码则无论是否是读操作或是写操作, 都要进行加锁, 这样就导致了加锁的性能消耗过大
所以对于根据读写操作进行加锁的思想, 引出了 双检查锁机制
1 | //双检查锁,但由于内存读写reorder不安全 |
但是对于双检查锁机制, 可能会有这样的疑问, 既然已经检查过 p 是否为空了, 为什么还要检查一次呢? 不妨模拟一下多线程环境下对于这段代码的访问顺序, 不难发现, 虽然在 p 为空时进行了加锁, 但是依旧可以有多个线程同时进入第 4 行, 既然进入了第 4 行, 如果不再进行检查, 即使加锁, 不同的线程也会在拿到锁之后执行完第一个 if(p==nullptr)
中的代码, 依旧会出现该对象被多次创建的问题, 所以双检查锁机制是必要的
C++ 11版本之后的跨平台实现 (volatile)
在使用了双检查锁机制后, 对于单例模式的实现似乎就已经无懈可击了, 不过在后来的使用中, 这种实现则被发现并不是绝对正确的. 问题的来源来自于编译器的优化方式, 对于不同版本的编译器, 其在底层创建对象的时候, 可能并不是先构造对象内存空间, 然后调用构造函数, 而是可能以某一种顺序执行, 这就带来了问题.
由于构造对象的过程并不是原子操作, 所以当线程第一次创建对象时, 时间片耗尽, 另一线程要去获取该对象, 由于此时 p 已经不为空, 则将会把一个没有进行初始化的对象返回, 从而导致错误, 为了解决这个问题, c++ 11 中采用了原子操作的方式
1 | //C++ 11版本之后的跨平台实现 (volatile) |
上面的方式实现又太过复杂, 由于 c++11 之后在创建对象时强制将其设置为原子操作, 故直接使用下面的简略写法即可
1 | Singleton Singleton::getInstance() { |