我们应该知道,C++中有21种设计模式,常见的有单例模式、迭代器模式、工厂模式、抽象工厂模式、观察者模式。今天我们先来说一下单例模式。
单例模式(Singleton)是设计模式中最为简单、最为常见、最容易实现的模式。单例模式就是怎样去创建一个唯一的变量(对象),即类只能实例化一个对象
1.单例模式实现思路:
1.屏蔽构造函数:
屏蔽生成对象的方法(私有化):将构造函数和拷贝构造函数写在私有访问限定符下,拷贝构造函数可以只写声明。
2.在类中公有访问限定符下提供一个生成对象的接口:
该接口不能依赖对象调用,即不能返回类类型的指针或引用。所以要用静态方法返回,提供一个静态方法来让外界获取对象实例
单例模式分为两种:
饿汉模式:先把对象(面包)创建好,等我要用(吃)的直接直接来拿就行了。
因为饿汉模式可能会造成资源浪费的问题,所以就有了懒汉模式
懒汉模式:先不创建类的对象实例,等你需要的时候我再创建。
2.饿汉模式的实现
class SingleTon//饿汉
{
public:
static SingleTon* getInstance()
{
return psingle;
}
private:
SingleTon(){}
SingleTon(const SingleTon&);//拷贝构造
static SingleTon* psingle;//指针标识唯一对象
};
SingleTon* SingleTon::psingle=new SingleTon();//静态成员变量在类外进行初始化
静态成员变量初始化时生成的唯一对象是在程序加载时就已经生成好了,即main函数执行前就生成好了,所以一定是线程安全的
因为线程是进程执行过程中产生的,主函数执行前唯一对象已经生成了。
它的缺点就是浪费资源,可能它生成的对象后面就没用,造成资源的浪费。
3.懒汉模式的实现
class SingleTon//懒汉
{
public:
static SingleTon* getInstance()
{
if(psingle==NULL)
{
psingle=new SingleTon();//用的时候才生成对象
}
return psingle;
}
private:
SingleTon(){}
SingleTon(const SingleTon&);
static SingleTon* psingle;//指针标识唯一对象
};
SingleTon* SingleTon::psingle=NULL;
懒汉模式又称延时加载,线程不安全。所谓线程不安全指:当唯一实例尚未创建时,如果有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。
《改进1》双重锁机制的单例模式
为了保证线程安全,可在生成对象前加锁,对象生成后解锁,但加锁解锁产生的开销,会导致效率变差,又线程不安全是只有在第一次生成对象时才会发生,所以我们只在第一次生成对象时进行加锁和解锁就行,即在外层再加个判断语句。这就是双重锁机制的单例模式,如下:
class SingleTon//懒汉
{
public:
static SingleTon* getInstance()
{
if (psingle == NULL)
{
//lock();
if (psingle == NULL)
{
psingle = new SingleTon();
}
//unlock();
}
return psingle;
}
private:
SingleTon(){}
SingleTon(const SingleTon&);
static SingleTon* psingle;//标识唯一对象;
};
SingleTon* SingleTon::psingle = NULL;
两次if()判空的作用
第一次判断singleton是否为null:
第一次判断是在Synchronized同步代码块外进行判断,由于单例模式只会创建一个实例,并通过getInstance方法返回singleton对象,所以,第一次判断,是为了在singleton对象已经创建的情况下,避免进入同步代码块,提升:效率。
第二次判断singleton是否为null:
第二次判断是为了避免以下情况的发生。
(1)假设:线程A已经经过第一次判断,判断singleton=null,准备进入同步代码块.
(2)此时线程B获得时间片,由于线程A并没有创建实例,所以,判断singleton仍然=null,所以线程B创建了实例singleton。
(3)此时,线程A再次获得时间片,犹豫刚刚经过第一次判断singleton=null(不会重复判断),进入同步代码块,这个时候,我们如果不加入第二次判断的话,那么线程A又会创造一个实例singleton,就不满足我们的单例模式的要求,所以第二次判断是很有必要的。
这样就既解决了饿汉模式的资源浪费问题又解决了懒汉模式的线程不安全问题。
但同时双重锁机制的单例模式又会出现新的问题:构造的唯一对象可能不是完整的。因为计算机系统中为了提高计算机系统性能,编译器、处理器、缓存会对程序指令和数据进行重排序,而对象的初始化操作并不是一个原子操作(可能会被重排序)
实例化一个对象可分为三个步骤:
(1)分配内存空间
(2)初始化对象(调用构造)
(3)将定义的对象的指针变量指向刚分配的内存空间(相当于‘=’赋值操作,使之对应起来)
上述步骤可能会重排序,变成:
(1)分配内存空间
(2)将定义的对象的指针变量指向刚分配的内存空间
(3)初始化对象
因此可能存在这种情况:一个线程正在构造对象的过程中(构造方法还没有执行完),另一个线程检查时看见了psingle为非NULL。即对象可能被非安全发布(对象并不完整就被其他线程使用)。
《改进2》使用volatile关键字禁止编译器优化
进一步优化:可以在使用volatile关键字禁止编译器进行优化,此时重排序被禁止,所有的写操作都将发生在读操作之前。
private:
volatile static SingleTon* psingle;//标识唯一对象;
至此,双重锁机制就可以完美工作了。