本文参考自《大话设计模式》,想借此记录一下对书本内容的理解,并以自己项目为例子采用C++语言进行举例。
概念
单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。通俗点讲,在程序当中创建了一个类,我们希望它仅能被实例化一次,产生一个对象。我们不能靠程序员去控制自己只能实例化一次,这是非常不保险的,我们需要代码内在机制帮助我们去控制这样的行为,这就是设计模式的意义。假设一个项目当中只能存在一个AGV(无人小车)对象。
三个要点
- 某个类只能有一个实例
- 它必须自行创建这个实例
- 它必须自行向整个系统提供对这个实例的访问
方法
1.让new失效
所有类都有构造函数,如果没有编写构造函数,则编译器会使用默认构造函数。首先考虑到的是将构造函数写成private,这样堵死了外界利用new创建此类实例的可能。因为new实质上也是调用构造函数。
public class AGV{
private AGV(){}
}
2.建立静态变量
通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的方法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法。
我们在类当中去保留这个唯一的实例化对象指针,并提供public函数去实例化/访问它,即设立一个static成员。在类中的static成员,代表所有的对象共用这一个成员,静态函数同理。
构造函数是私有的,但是GetInstance函数是内部成员函数,可以调用私有变量。函数内部首先要判断agvObject是否为空,如果为空,证明之前并没有创建,则通过new实例化一个对象。如果不为空,则证明之前已经创建了,则直接返回agvObject。此函数是获得本类实例的唯一全局访问点。
public class agvClass{
private static agvClass* agvObject;
private agvClass(){}
publiac static agvClass GetInstance() {
if (agvObject == null) {
agvObject = new agvClass();
}
return agvObject;
}
}
多线程时的单例
1.问题提出
在多线程时,如果多个线程同时访问GetInstance函数,则有可能造成创建多个实例。这和单例模式的初衷的不符的。为什么会造成这种情况呢?
想象这样一个场景,假设A线程已经运行到箭头所指的部分,此时判断语句已经结束,实例化new语句还没开始。B线程横刀夺爱,抢走了CPU使用权,同样也调用GetInstance函数。此时agvObject依然为null,因为A线程并没有运行new语句。那B线程就实例化了一个agvClass。
回到A线程,因为判断语句已经运行过了,所以不需要再次判断agvOject是否为空,也就错过了挽救的机会。直接运行new语句,再次实例化agvClass。这样系统就存在了两个agvClass。
2.线程单锁
确保当一个线程位于代码的临界区时,另外一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待,直到该对象被释放。
publiac static agvClass GetInstance() {
lock();
if (agvObject == null) {
agvObject = new agvClass();
}
unlock();
return agvObject;
}
3.双重锁定
如果是单锁,每次调用GetInstance函数都需要lock,影响性能。因此出现了Double-Check Locking(双重锁定)。
publiac static agvClass GetInstance() {
if (agvObject == null) {
lock();
if (agvObject == null) {
agvObject = new agvClass();
}
unlock();
}
return agvObject;
}
如果实例不为空,直接返回agvObject,不需要加锁解锁等步骤。只有在实例未被创建的时候再加锁处理。那为什么需要判断两次agvObject为空?
第一次判断是在普通情况下,防止多次加锁解锁影响性能而设定的。
第二判断是用于两次线程同时进入GetInstance情况设定的。假设A线程停留在如箭头所指的部分,此时new语句还未运行。和上面情况类似,B线程横刀夺爱,也调用了GetInstance,它首先会通过第一个if判断,因此此时实例尚未创建,但会停留在lock这里,排队等候。当A线程完成实例的创建,B继续运行,如果此时没有第二次判断,就会创建第二个实例,不符合单例模式。
懒汉模式和饿汉模式
1.懒汉模式
上面提到的模式就是懒汉模式,就是第一次用到类的示例才实例化。注意到,前面new语句放在GetInstance函数,这个函数需要外界调用才会发挥作用,然后new一个实例。形象点记忆,懒汉就是不到万不得已就不会去实例化类,我用不上那就先不管。懒汉模式是不安全的实现方式,是线程不安全的,需要加锁。
2.饿汉模式
类定义的时候就实例化,本身是线程安全,不需要加锁。饿汉,就是不管三七二十一,这个类编译的时候就实例化,不考虑后面用不用得上。用法如下所示,直接在类外实例化,即初始化即实例化。
public class agvClass{
private static agvClass* agvObject;
private agvClass(){}
publiac static agvClass GetInstance() {
return agvObject;
}
}
agvClass* agvClass::agvObject = new agvClass;
3.选择
懒汉:在访问量比较小,采用懒汉模式。到有需要的时间才实例化对象,那它就不会提前占据内存空间,代价就是后续每次访问都会判断是否为空,增加时间成本。这是以时间换空间。
饿汉:在访问量比较大,或者可能访问的线程比较多,采用饿汉模式。就算没用上实例对象,也会进行实例化,这是要占据一定内存的。但在后面需要使用的时候,就不需要判断之类语句,所以非常快速。这就是以空间换时间。
|