单例-怎么用单例
单例的实现
在实现一个单例时,需要从如下角度来考虑单例的实现是否合理:
1.不应该提供创建多个实例的接口。主要是两部分内容,分别是要将构造函数设为private属性,防止在别处通过new创建实例;在创建实例时要考虑线程安全问题,避免在多线程环境中运行时,同一时间多个线程竞争,导致创建了多个实例。
2.考虑是否支持延迟加载。根据系统性能要求的不同,有的系统要求启动速度快,那就不能在启动时创建实例;有的系统要求获取实例时快,那就不能在调用单例时创建实例。
3.考虑Getinstance性能是否高(是否加锁),如果每次获取单例都要加锁解锁,那么对性能的影响是很大的。
饿汉式
懒汉式单例从生命周期来讲,是“与天地同寿的”,在调用main函数之前,单例的构造函数就已经被调用必完成了初始化。(在msvc C++中,真正的函数入口是msvcStartup而不是main函数,在调用main函数之前,
会执行全局对象的初始化,具体实现是这样的:在编译时,将全局构造函数的指针放在一个特殊的段(.ctors段)中,在调用msvcStartup时,遍历该段,将里面所有的构造函数执行一遍)。具体实现如下:
//Signleton.h class Signleton { public: virtual ~Signleton(); Signleton& Getinstance(); private: Signleton();//先藏起构造函数 }; //Signleton.cpp Signleton instance;//这个是放在cpp文件里面的全局变量。 Signleton& Signleton::Getinstance() { return instance;//不需要加锁因为在系统构造前就建好了实例 }
饿汉式由于是在系统启动之前就存在了,显然是不支持延迟加载的。与此同时,由于先于系统启动,也不需要
但是,C++中,全局构造函数的调用顺序是未知的,也就是说,如果这个单例中构造时依赖了其他单例,那这个行为就是未定义的。其次,如果单例构造时需要用到系统的一些参数,那这些参数是无法获得的,因为系统都还没启动。。。
那么,饿汉式需不需要考虑构造时线程问题呢?不用的,肯定是在主线程执行的时候由主线程创建的,没别的线程来竞争。
那支持延时加载吗?不支持的,他比谁都先加载出来
那需要加锁吗?不需要的,因为不管多少个线程同时调用GetInstance,Getinstance返回的结果都是一样的,都是系统启动前加载的那个。
懒汉式
懒汉式恰恰和饿汉式相反,生存周期可以说是“应运而生”,就是说只有在第一次调用Getinstance时才会被实例化。具体实现如下:
//Signleton.h class Signleton { public: virtual ~Signleton(); Signleton& Getinstance(); private: Signleton();//先藏起构造函数 private: Signleton* mInstance; }; //Signleton.cpp Signleton* Signleton::mInstance = nullptr; Signleton& Signleton::Getinstance() { if (nullptr == mInstance) { mInstance = new Signleton; } return *mInstance; }
那么,还是使用你是三个问题来评判懒汉式的实现:
首先是是否支持多线程环境?答案是不支持的,在多线程环境下,如果两个线程th1
和th2
同时执行并且都执行到了 nullptr == mInstance
这个判断的时候,th1
和th2
将同时、分别生成两个实例instance1
和instance2
。所以懒汉式不支持多线程环境
其次是判断懒汉式是否支持延迟加载?由于是在第一次调用的时候实例化的,满足延迟加载的条件。
双重检查式
为了满足在多线程环境下运行的需求,现在对懒汉式实现打点补丁。具体实现如下:
//Signleton.h class Signleton { public: virtual ~Signleton(); Signleton& Getinstance(); private: Signleton();//先藏起构造函数 private: Signleton* mInstance; mutex mMutex; }; //Signleton.cpp Signleton* Signleton::mInstance = nullptr; Signleton& Signleton::Getinstance() { if (nullptr == mInstance) {//注意这里是判断了两次 mMutex.lock(); if (nullptr == mInstance) { mInstance = new Signleton; } mMutex.unlock(); } return *mInstance; }
打了补丁之后的懒汉式单例,既可以支持延时加载,又可以在多线程环境下运行。在调用
GetInstance的时候,先判断是否已经实例化,如果没有实例化,就加锁,并且实例化。实例化的时候,其他线程调用mMutex.lock()将会被阻塞住。实现在多线程环境下只实例化一个对象的目的。
可是,为什么是两次判断?Getinstance像下面实现不好吗?
Signleton& Signleton::Getinstance() { mMutex.lock(); if (nullptr == mInstance) { mInstance = new Signleton; } mMutex.unlock(); return *mInstance; }
这样的话,每次调用Getinstance都会加锁解锁,开销很大,降低了系统效率。
如果要提升系统效率,像下面那样实现行不行?
Signleton& Signleton::Getinstance() { if (nullptr == mInstance) { mMutex.lock(); mInstance = new Signleton; mMutex.unlock(); } return *mInstance; }
其实也不行,这样也会存在同时多个线程通过nullptr == mInstance
判断的情况,这种写法根本不能支持多线程环境,具体点说就是线程th1,th2同时都通过了外层判断到了内层,此时th1拿到锁,th2阻塞,当th1实例化完成时,th2就会直接再新建一个实例并返回,就是说Signleton被实例化了两次。
所以,两次判断,也是有两个目的的,首先,要在没有实例化时,实例化对象,通多外层判断加快第二次调用Getinstance的性能。其次,内层判断,是要防止当前线程拿到锁的那一刻,别的线程没有实例化对象完成。
局部变量式
从效果上来看,双重加锁仿佛已经是无敌了,及支持延时加载,也可以支持多线程并发,同时加锁也仅仅是在第一次调用,效率较高。还有一种写法,原理上和双重加锁类似,但是更加简洁,具体实现如下:
Signleton& Signleton::Getinstance() { static Singleton value; //静态局部变量 return value; }
上面的式子,利用了局部静态变量在第一次被调用时就会初始化的特点,而且C++11标注明确规定了静态变量的实例化是线程安全的,所以说只有第一次加载且线程安全这两个条件,都利用C++自身的特性实现了,所以我说思想和双重检测类似,但是写法更简单。