单例模式_没有想象中那么简单
1、至多有且仅有一个唯一的实例,不一定就一定要将构造函数的访问权限定为private,我们可以通过各种各样的方法来实现它的构造函数只能成功地执行一次。但是,将构造函数定为private直观省事。
private: Singleton(void);
2、既然我们将构造函数定为private,外部是无法构建它的实例。因此,我们就需要一个不需要它的实例也能够被访问的函数,这就是静态函数。能够被外部访问,毫无疑问它的访问权限应该是public(虽然能过通过一系列强制转换直接通过地址来调用函数,此处忽略不计)
public: static Singleton* getInstance(void);
3、我们只构造一次,因此需要用一个成员变量来记录它的地址。上面已经提到,我们使用静态函数来向外部提供访问接口,众所周知,在静态函数中只能使用静态变量。对于这个静态成员变量的访问权限没有严格的定义,但是,为了体现封装性,我们再次将其定位private。
private: static Singleton* _instance;
4、在此贴上整体单例类
class Singleton { public: ~Singleton(void); static Singleton* getInstance(void); private: Singleton(void); private: static Singleton* _instance; }; Singleton::Singleton(void) { std::cout<<"I am Singleton !\n"; } Singleton::~Singleton(void) { if (NULL != _instance) { delete _instance; _instance = NULL; } } Singleton* Singleton::getInstance(void) { if (NULL == _instance) { _instance = new Singleton(); if (NULL == _instance) { std::cout<<"Singleton instance new is failed !\n"; } } return _instance; }
5、经过上面3个步骤就能够完成粗糙的单例了,为什么是粗糙的呢?内存回收、多线程运用。
当一个项目越来越庞大,使用的单例越来越多,逻辑关系越来越复杂,什么时候销毁这些单例会使我们迷惑;上面的单例实现在多线程的环境下可能产生多个实例。
6、对于内存管理使用较为简单的是使用局部静态变量,静态变量只初始化一次,存储空间不在栈,在程序退出时将自动被销毁。
Singleton* Singleton::getInstance(void) { static Singleton instance; return &instance; }
还有一种方法是在单例类内写一个内部类并持有一个该内部类的静态成员,但是感觉上怪怪的。
7、对于多线程,一种是使用饱汉模式(在使用到单例实例前就创建好实例,上面的实现位饿汉模式,需要使用实例是才创建实例),下面是加锁
Singleton* Singleton::getInstance(void) { if (NULL == _instance) { lock(); if (NULL == _instance) { _instance = new Singleton(); if (NULL == _instance) { std::cout<<"new Singleton() failed !\n"; } } unlock(); } return _instance; }
为什么要在lock的都两边加上 if (NULL == _instance) 呢?如果只用一个判空,lock放在判空前,那么我们在每一次获取实例时都要执行lock,大大滴降低了效率;那么放在判空后面,那么将可能有多个线程在第一个实例创建出来前通过判空依次等待创建多个实例。
8、是不是应该打完收工了?套用一句名言:一切才刚刚开始!
9、我们先来唠叨两段话
1)顺序流入, 乱序流出: 一般情况下, 指令乱序并不是CPU在执行指令之前刻意去调整顺序。 CPU总是顺序的去内存里面取指令, 然后将其顺序的放入指令流水线. 但是指令执行时的各种条件, 指令与指令之间的相互影响, 可能导致顺序放入流水线的指令, 最终乱序执行完成。
2)编译器乱序:如果两条像这样有依赖关系的指令挨得很近, 后一条指令必定会因为等待前一条执行的结果, 而在流水线中阻塞很久.。而编译器的乱序, 作为编译优化的一种手段, 则试图通过指令重排将这样的两条指令拉开距离, 以至于后一条指令执行的时候前一条指令结果已经得到了,那么也就不再需要阻塞等待了。一下位几种可能的原因:
(2.1)避免寄存器数据发生溢出情况
(2.2)保障指令集能够更为顺利地流水线执行
(2.3)公共子表达式消除
(2.4)降低生成的可执行文件的大小
10、上面的的实现使用C++实现,总所周知,C++可是传说中的高级语言,这个高级很是深奥(⊙o⊙)哦(在我看来,所谓的高级粗犷的理解就是一般人看不懂,就是下面这张图)
所以我们的实现计算机不认识,那么编译器就上场了。编译器会为我们的实现到机器语言做中间的桥接工作(用桥接不大恰当,将就下吧),那么这个桥接工作它干了啥子?这个我还真不知道,大概完整的正确行为只有编译器的开发者才能够完全了解。那么问题来了,编译器的这个桥接工作能够与我们众多程哥的期望没有差错吗?肯定有(老婆多没有这么听话的时候吧),这个差错包括有利与不利的。
11、这一行(序列点)_instance = new Singleton(); 能够完成3件事情
1)分配内存:分配一个Singleton对象需要大小的内存空间
2)构建对象:构造函数构建Singleton对象放在上面空间里
3)地址传递:将_instance指向上面的这块内存空间的地址
我们希望它的执行步骤是1->2->3,这是我们程哥的梦,高于一切(能不能比老婆还高有待考究)
12、_instance = new (std::nothrow) Singleton();公司要求我们尽可能的这样使用new,可以不抛出异常
13、有没有想过11的步骤2与3的执行顺序对换1->3->2,会是一个什么情况?在某些情况下,编译器的优化顺序可能会是这个样子的,11仅是一个简单的例子
1)线程first通过了第一个 if (NULL == _instance)
2)first获得lock(),执行到3被无情的挂起了
3)线程second在第一个 if (NULL == _instance) 出被否决后返回了_instance
4)crash.......又是一个不眠之夜啊
14、意识到这个问题的时候,我第一个反应就是——饱汉单例模式,的确能够解决问题
15、举个例子
1)你三急(屁急就算了),方圆百里除了眼前一个收门票游乐场里有厕所(坑位足,不排队)外没有厕所,你的移动速度不大于10km/h
2)游乐场两种门票:入门票¥10、全场票¥250(这两种票的区别就是字面上的意思)
3)作为一个四有青年,你选择进入游乐场解决并豪爽地买下了提供一条龙服务的全场票
4)你以风骚的走位来到战场,一个大招五杀
5)出门左拐
14的解决方案可能出现这种情况,还不如当地大小便呢!
16、那么想个简单的办法绕过编译器的优化,牛人另当别论,至少以目前的我还是斗不过那些几十年如一日成天想着优化代码的上帝.....
17、我们来唠叨一个关键字volatile
public: volatile Singleton* volatile getInstance(); private: static volatile Singleton* volatile _instance;
18、曾今看到过 LINUX内核内存屏障 这一译文 http://blog.chinaunix.net/uid-9918720-id-1640912.html
内存屏障也是这种问题的一个解决方案,不过这值不值得,会不会产生15中的类似现象呢?
Singleton* Singleton::getInstance(void) { if (NULL == _instance) { lock(); if (NULL == _instance) { Singleton* inter = new Singleton(); //insert memory-barrier code _instance = inter; if (NULL == _instance) { std::cout<<"new Singleton() failed !\n"; } } unlock(); } return _instance; }
总而言之,言而总之,这就是我理解的单例模式,简单吗?说简单也简单,说不简单也不简单。(如有错误,请回复指正)