单例模式细说
单例模式,可以说是最常用的设计模式了,在工程中大量被用到。虽然单例模式比较简单,但是如果单例模式没用好,那也会遇到很多莫名其妙的坑,这里就来讲讲单例模式的一些注意点。
1 局部静态变量, 函数内创建的局部static变量,只创建一次。这种不需要额外的全局变量和成员变量。
2 函数内new 出来的对象,需要根据指针判断是否创建过。这种需要全局变量或者成员变量来保存这个指针。
3 以上二者结合,局部静态的指针,再通过new的方式创建。也不需要额外的变量。
4 boost或其他第三方库提供的单例,如boost::detail::thread::singleton;
5 std::call_once,C++11的标准库中的方式。
一般单例都是拒绝多个实例,所以不允许赋值和拷贝,构造函数一般外部不能直接使用的,即不能直接在外部构造对象。通常可以继承boost::noncopyable,来防止这种情况。或者自己将构造函数、拷贝构造、和赋值操作符定义为私有,或者使用C++11的delete关键字加在函数末尾 如Singleton( const Singleton& ) = delete;防止编译器自动生成这几个函数。
如果只在主线程用,那可以不加锁,如果多线程用,需要注意多次创建问题。一般多线程使用单例不加锁的话,需要在程序开始时单独初始化,如果别人不小心在之前创建了另外一个线程也用了此单例,可能导致多次创建的问题。
另外,如果单例都在开始时初始化,可能导致性能问题,集中初始化可能占用较多时间。
所以单例最好能加锁确保被人不会误用。最好是加锁的,加锁能真正做到用到才初始化,所谓的懒汉模式。但是加锁又有性能问题,所以有个方法即加锁,性能又高,即在加锁前增加判断,如果创建过了就不加锁了:
但是双重检查真的万无一失吗?更深入的研究发现,有时两次检查和加锁的这种方式也不是绝对的线程安全的!因为编译器优化可能导致new的步骤被打乱。
instance = new Singleton()至少包含三个步骤:
1为Singleton分配内存
2在内存中创建Singleton对象
3让实例引用Singleton对象
例如,出于优化原因,处理器可以将步骤重新排序到序列1->3->2。
这样,在第一步骤中将分配内存,而在第二步骤中,实例极有可能是个不完整的实例(还没初始化完成)。如果在那个时候另一个线程试图访问单例,它会比较指针并得到答案为真。所以,另一个线程有一种错觉,即它以为它正在处理一个完整的单例。结果很简单:程序行为未定义。
那有没有线程安全的单例呢?后面会讲到。
1 直接在类中增加宏定义,来增加类的GetInstance和ReleaseInstance成员函数。这样做会侵入代码,修改原来的类。还需要注意对象被拷贝的问题。
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_once和std::once_flag来实现单例: