保证一个类仅有一个实例对象,并且提供一个访问它的全局访问点,并且使用静态数据成员来表达这唯一的一个对象。可以类比于windows的任务管理器,无论多少次打开,始终只显示一个窗口。
一、实现单例模式需要注意的点:
(1)屏蔽构造和拷贝构造函数;
(2)在类中提供一个接口生成唯一的对象,不能返回类类型;
(3)不能依赖对象调用;
实现单例解决的问题是怎样创建一个唯一的对象?
二、单例模式结构图:
单例模式典型的结构图。在Singleton 模式的结构图中可以看到,通过维护一个 static 的成员变量来记录这个唯一的对象实例。通过提供一个 staitc 的接口 instance 来获得这个唯一的实例。
三、单例模式的实现:
单例模式下的对象加载又分为懒汉模式和饿汉模式。
1、懒汉模式:第一次用到类实例的时候才会去实例化,也称延时加载;
2、饿汉模式:在单例类定义的时候就进行了实例化,也称贪婪加载;
四、两者的具体如下:
1、饿汉模式实现:符合贪婪加载即就是提前加载;
缺点:当提前加载好的对象在后期没有用到的时候会造成内存浪费;
优点:不存在线程安全问题;
class Singleton
{
public:
static Singleton* GetInstance()
{
return single;
}
private:
Singleton(){};
Singleton(const Singleton&);
static Singleton *single;
};
Singleton *Singleton::single = new Singleton();
3、懒汉模式的实现:符合延时加载即就是用时才生成;
缺点:存在线程安全的问题;
为什么是线程不安全呢?
假设我们有两个线程,线程1进入要创建实例对象,当线程1将要创建时,cpu切换给了线程2,此时线程2也要创建实例对象,single为空,线程2成功创建了对像后。Cpu切换给了线程1,线程1继续进行对象的创建,这样最终会产生2个对象。这种结果就不是我们单例模式所要实现的目标。因此说它是线程不安全的。如下图所示:
那么,我们可以通过锁机制来实现线程安全,而且必须用二重锁机制。
class Singleton
{
public:
static Singleton* GetInstance()
{
if(single == NULL)
{
//lock()
if(single == NULL)
{
single = new Singleton();
}
//unlock();
}
return single;
}
private:
Singleton(){};
Singleton(const Singleton&);
static Singleton *single;
};
Singleton *Singleton::single = NULL;
但是对于懒汉模式下的二重锁机制加载也会存在一个微小的问题:就是在single = new Singleton();时;因为new不是一个原子操作,在进行new的时候会有以下三步:
(1)分配内存空间;(2)调用构造函数进行初始化 (3)将定义的对象single指向刚分配的内存空间
但是在计算机系统中为了提高计算机系统性能,编译器,处理器,缓存会对程序指令和数据进行重排序,而对象的初始化操作并不是一个院子操作。因此上述的三步操作可能会被重排序为:(1) (3) (2)
所以可能会存在这样的情况:一个线程正在构造对象的过程中(构造方法还未调用),另一个线程检查时看见了single为非空。也就是说对象可能会被非安全发布(对象并不完整就被其他线程所使用)。
解决这一问题:使用volatile关键字,这个关键字的作用就是禁止编译器优化;此时,系统就被禁止进行重排序,所有的写操作都将发生在读操作之前。
volatile static Singleton* single;//标识唯一对象;
volatile关键字能保证可见性和有序性;
在这里其保证了有序性,两层含义:
第一当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
第二在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。