单例模式细说

背景

单例模式,可以说是最常用的设计模式了,在工程中大量被用到。虽然单例模式比较简单,但是如果单例模式没用好,那也会遇到很多莫名其妙的坑,这里就来讲讲单例模式的一些注意点。

 

单例模式创建对象有分几种情况:

1 局部静态变量, 函数内创建的局部static变量,只创建一次。这种不需要额外的全局变量和成员变量。

2 函数内new 出来的对象,需要根据指针判断是否创建过。这种需要全局变量或者成员变量来保存这个指针。

3 以上二者结合,局部静态的指针,再通过new的方式创建。也不需要额外的变量。

4 boost或其他第三方库提供的单例,如boost::detail::thread::singleton;

5 std::call_onceC++11的标准库中的方式。

 

单例模式如何防止多实例:

一般单例都是拒绝多个实例,所以不允许赋值和拷贝,构造函数一般外部不能直接使用的,即不能直接在外部构造对象。通常可以继承boost::noncopyable,来防止这种情况。或者自己将构造函数、拷贝构造、和赋值操作符定义为私有,或者使用C++11的delete关键字加在函数末尾 如Singleton( const Singleton& ) = delete;防止编译器自动生成这几个函数。

 

单例模式的线程安全

如果只在主线程用,那可以不加锁,如果多线程用,需要注意多次创建问题。一般多线程使用单例不加锁的话,需要在程序开始时单独初始化,如果别人不小心在之前创建了另外一个线程也用了此单例,可能导致多次创建的问题。

另外,如果单例都在开始时初始化,可能导致性能问题,集中初始化可能占用较多时间。

所以单例最好能加锁确保被人不会误用。最好是加锁的,加锁能真正做到用到才初始化,所谓的懒汉模式。但是加锁又有性能问题,所以有个方法即加锁,性能又高,即在加锁前增加判断,如果创建过了就不加锁了:

单例模式细说

 

但是双重检查真的万无一失吗?更深入的研究发现,有时两次检查和加锁的这种方式也不是绝对的线程安全的!因为编译器优化可能导致new的步骤被打乱。

instance = new Singleton()至少包含三个步骤:

1为Singleton分配内存

2在内存中创建Singleton对象

3让实例引用Singleton对象

例如,出于优化原因,处理器可以将步骤重新排序到序列1->3->2。

这样,在第一步骤中将分配内存,而在第二步骤中,实例极有可能是个不完整的实例(还没初始化完成)。如果在那个时候另一个线程试图访问单例,它会比较指针并得到答案为真。所以,另一个线程有一种错觉,即它以为它正在处理一个完整的单例。结果很简单:程序行为未定义

那有没有线程安全的单例呢?后面会讲到。

 

单例模式代码惯用法:

1 直接在类中增加宏定义,来增加类的GetInstanceReleaseInstance成员函数。这样做会侵入代码,修改原来的类。还需要注意对象被拷贝的问题。

单例模式细说

2 通过继承方式,继承父类的接口。这样不用修改原有的类,也不用担心对象被私自创建与拷贝,因为父类用已经继承了noncopyable。

单例模式细说

单例模式细说

3 不通过继承,直接通过单例类加模板来使用单例,这种不需要修改原有的类,不过要注意对象被拷贝或私自构造的情况:

typedef boost::detail::thread::singleton<MyClass> MyClassSingleton;

MyClassSingleton::instance();

 

单例模式的懒汉与饿汉模式:

懒汉:故名思义,不到万不得已就不会去实例化类。也就是说在第一次用到类实例的时候才会去实例化;

饿汉:饿了肯定要饥不择食。所以在单例类定义的时候就进行实例化(如全局变量或静态成员变量初始化时直接构造一个)。总结就是,很早初始化,不论后面是否使用。

 

单例模式C++11增强:

旧版C++中不加锁的单例可能被重复创建。在C++11中,支持了线程安全的单例类。

局部的static 静态变量在C++11时多线程情况下依然正确运作,不会被多次构造

这种单例模式称作Meyers模式,C++11时的最佳选择:

单例模式细说

C++11还提供了std::call_oncestd::once_flag来实现单例

单例模式细说