单例模式
在面向对象编程中,有时候我们希望达到这样一种效果,一个类只有一个实例化的对象,比如线程池,缓存等,所以人们人为规定,这些类有且只有一个唯一的实例。这种设计模式被称为单例模式。
单例模式的特点
- 使用单例模式的类没有公开的构造函数,所以不能创建该类的实例
- 同理,使用单例模式的类也没有公开的拷贝函数和赋值函数
- 使用单例模式的类需要提供一个公有方法,让外部能访问到这个唯一的实例
这样,我们可以大致写出这个类的大致结构,它的构造函数,赋值函数和拷贝函数都是私有的,只有获取实例的接口是公有的。唯一的实例要使用static关键字标记,以保证唯一性。
1 class Singleton{ 2 3 public: 4 Singleton*getInstance(); 5 private: 6 Singleton(); 7 Singleton(const Singleton&); 8 Singleton& operator =(const Singleton&); 9 static Singleton*_instance; 10 };
单例模式分类
单例模式有两种主要的实现方法,懒汉模式和饿汉模式。
1. 懒汉模式
懒汉模式的特点是当外界调用时才进行实例化。下面是常见的一种懒汉模式的写法
1 Singleton*Singleton::getInstance() 2 { 3 if (_instance == nullptr) 4 _instance = new Singleton; 5 return _instance; 6 }
懒汉模式的线程安全问题
上面这种写法在单线程模式下是没有问题的,但多线程环境下是不安全的,假如这个唯一的实例还没有创建,这时有两个线程同时调用GetInstance方法,有可能会发生下面这种情况:
这种情况下,线程A,B可能会创建两个不同的对象,导致程序错误。
解决这个问题的一个很自然的想法是对它加锁。
1 mutex m; 2 Singleton*Singleton::getInstance(){ 3 m.lock(); 4 if (_instance == nullptr) 5 _instance = new Singleton; 6 m.unlock(); 7 return _instance; 8 }
但是加锁又会带来另外的性能问题,如果每个线程每次获取实例都加锁,有可能造成阻塞的发生。实际上,上锁的目的是为了防止有多个线程在实例未被初始化的情况下,同时对他进行初始化,如果实例已经被创建了,就不需要考虑这个问题了,所以就可以采用二次加锁的方法来提高程序的性能。
二次加锁检查
1 Singleton*Singleton::getInstance(){ 2 3 if (_instance == nullptr) 4 { 5 m.lock(); 6 if (_instance == nullptr) 7 { 8 _instance = new Singleton; 9 } 10 m.unlock(); 11 } 12 return _instance; 13 }
二次加锁检测的内存乱序问题
虽然二次计算加锁检测可以避免阻塞,但是可能会造成内存乱序问题,究其原因还是出在
_instance = new Singleton
这个语句上,这个语句分三个步骤执行
1. 分配Singleton类型对象所需的内存
2. 在分配的内存出构造Singleton对象
3. 将分配的内存的地址赋给指针_instance
其中2,3两个步骤的顺序是不一定的,这就可能出现一种情况,即_instance已经得到了地址,但是Singleton对象却还没有构造出来。这就可能出现严重的bugger。
推荐的懒汉模式写法
Soctt Meyers 在 《Effiective C++》中提出了一种高效便捷的懒汉模式实现方法。这种方法直接在getInstance中定义了一种static的Singleton变量并返回
class Singleton{ public: Singleton*getInstance(); private: Singleton(); Singleton(const Singleton&); Singleton& operator =(const Singleton&); //static Singleton*_instance; }; Singleton*Singleton::getInstance(){ static Singleton local_instance; return &local_instance; }
注意,该模式在C++11前可能发生错误。
2. 饿汉模式
饿汉模式的特点是一开始就对实例进行初始化,调用时直接返回这个构建好的实例。
class Singleton{ public: Singleton*getInstance(); private: Singleton(); Singleton(const Singleton&); Singleton& operator =(const Singleton&); static Singleton _instance; }; Singleton*Singleton::getInstance(){ return &_instance; }
饿汉模式不会面临线程安全的问题,因为实例从一开始就已经被创建好了。