单例模式:保证一个类只有一个实例,并提供一个该实例的全局访问点
实现方式:构造和拷贝构造设为私有,总共介绍四个版本,推荐使用最后一个。版本1、2、3、4全都是懒汉式的写法。
版本1:线程非安全版本 在多线程情况下可能会同时创建出多个对象,比如,现在有线程A和线程B,线程A进入到16行时, 线程B进入17行,会容易new出多个实例。
文章图片
版本2:线程安全,但锁的代价过高 在GetInstance()中使用局部变量锁, 保证同一时刻只有一个线程访问30-33行。
读变量没必要加锁,尤其是在高并发的情况下,代价还是挺高的。
文章图片
版本3:双检查锁,但由于内存读写reorder(重新排序)不安全(导致双检查锁的失效) 锁前检查,避免都是读取操作时锁代价过高的问题
锁后检查,避免两个线程同时进入,从而new了两个实例
文章图片
因为编译器优化,指令的执行顺序可能reorder(CPU执行指令的层次,而且线程是在指令层次抢时间片的) ,可能变成这样:分配内存->赋值->调用构造(理想应该是:分配内存->调用构造->赋值)
假设在reorder的情况下:线程1走到赋值,但还没调用构造的阶段,而线程2进来判断m_instance,此时它已经被复制 所以不为空,这时候线程2就直接返回m_instance,但事实上它还没构造出来……这就尴尬了,实际上因为它没有构造,肯定是不能用的。
总之,就是双检查锁 它欺骗了线程2……
怎么解决这个问题呢?
Java 和c sharp 添加了一个关键字:volatile
这样,编译时在编译的时候就知道,这个变量的整个赋值过程不能reorder,需要按照常规的流程走。
C++11之后 跨平台实现了volatile
还是挺复杂的哈……
文章图片
前3个版本均来自侯捷老师的视频,可以去观看一下,讲的灰常简单易懂
版本4:局部静态变量实现单例模式,线程安全(推荐使用)
class Singleton{
public:
~Singleton();
Singleton& (const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance()
{
static Singleton instance;
return instance;
}
private:
Singleton();
};
【设计模式|单例模式4种实现方式C++】原因是C++ 11标准中新增了一个特性叫Magic Static:如果变量在初始化时,并发线程同时进入到static声明语句,并发线程会阻塞等待初始化结束。
这样可以保证在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性,同时也避免了new对象时指令重排序造成对象初始化不完全的现象。并且相比较与使用智能指针以及mutex来保证线程安全和内存安全来说,这样做能够提升效率。
推荐阅读
- 兔老大的系统设计|兔老大的系统设计(一)健康度系统
- 软件设计的七大原则
- 设计模式|Builder建造者模式
- 设计模式|观察者(observer)模式(一)
- 设计模式|观察者(observer)模式(二) —— 实现线程安全的监听器
- C++|C++ stringstream
- c++|c++17操作文件并解析目录
- java基础|springboot优雅实现工厂模式,策略模式——利用spring自动注入list,map性质
- java|docker-compose安装教程