什么是单例模式
单例模式是一种常用的设计模式。它能够保证系统中应用该模式的类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
单例模式实现方式
单例模式的实现有两种方式:饿汉模式和懒汉模式。
饿汉模式
该种模式是不管你将来用不用,在程序运行之初就将对象创建好,程序启动时就创建唯一的实例对象。在访问量比较大,或者可能访问的线程比较多时,采用饿汉模式实现,可以实现更好的性能。以空间换时间。
因为我们想在运行之初就要创建好这个唯一实例,所以就要设置一个静态变量,并且把构造函数/拷贝构造函数/赋值运算符重载禁掉(设置成私有,只声明不实现,这样类外就不能调用,且类内也无法调用),这样就保证了实例创建的唯一方式。
再设置一个共有的静态成员函数,返回这个单例的地址,这样就能够保证操纵同一个实例了(设置成静态成员函数是因为,非静态成员函数需要通过对象才能够调用,但是我们又不能通过别的方式创建对象。)
class Hungry
{
public:
static Hungry* GetInstance()
{
return &onlyInstance;
}
private:
Hungry() {};
Hungry(const Hungry&);
Hungry& operator=(const Hungry&);
//C++11写法
Hungry(const Hungry&) = delete;
Hungry& operator=(const Hungry&) = delete;
private:
static Hungry onlyInstance;
};
Hungry Hungry::onlyInstance;
int main()
{
Hungry* a = Hungry::GetInstance();
return 0;
}
饿汉模式的优点:
1.简单
2.程序启动就设置好了单例,所以饿汉模式是线程安全的。
饿汉模式的缺点:
1.因为单例在程序启动的时候创建,所以可能会导致进程启动慢
2.如果有多个单例类对象实例启动顺序不确定。
懒汉模式
该模式是当首次想要使用单例的时候,才去创建单例。
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好(以时间换空间).
class Lazy
{
public:
static Lazy* GetInstance()//返回单例
{ //这里一定要是用双重检查的方式加锁,才能保证效率和线程安全
if (nullptr == OnlyInstance)
{
m_mutex.lock();//加锁
if (nullptr == OnlyInstance)
{
OnlyInstance = new Lazy;
}
m_mutex.unlock();//解锁
}
return OnlyInstance;
}
//实现一个内嵌的垃圾回收类
class CGarbo
{
public:
~CGarbo()
{
if (Lazy::OnlyInstance)
delete Lazy::OnlyInstance;
}
};
//定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象。
static CGarbo Garbo;
private:
//构造函数私有
Lazy() {};
//防拷贝
Lazy(const Lazy&) = delete;
Lazy& operator=(const Lazy&) = delete;
private:
static Lazy* OnlyInstance;//单例指针
static mutex m_mutex;//互斥量
};
Lazy* Lazy::OnlyInstance = nullptr;//初始化
mutex Lazy::m_mutex;
Lazy::CGarbo Garbo;
懒汉模式中,我们用一个静态指针变量来指向单例,静态指针在程序启动时初始化,定义一个GetInstance接口,返回单例指针。
需要注意的是,因为懒汉模式的单例是需要的时候才去创建,所以在首次创建实例的会后要考虑线程安全的问题。
static Lazy* GetInstance()//返回单例
{ //这里一定要是用双重检查的方式加锁,才能保证效率和线程安全
if (nullptr == OnlyInstance)//第一次检查
{
m_mutex.lock();//加锁
if (nullptr == OnlyInstance)//第二次检查
{
OnlyInstance = new Lazy;
}
m_mutex.unlock();//解锁
}
return OnlyInstance;
}
当多个线程并行执行的时候,可能他们都会判断实例未被创建,所以实例就会创建多个,但是这样就不满足单例了。
因此,想让懒汉模式显得线程安全,只需要将堆公共资源的操作原子化就可以了,即加锁,加锁的话让并行的线程变成串行来执行,这样就可以保证这个共有的接口每次只能由一个线程来调度,之后的线程就会被挂起等待,直到前面的线程将锁释放。
双重检查的原因:
第一层检查:
当一个线程运行到第一层检查,如果发现实例已经创建,就直接返回。
如果没有创建则通过第一层检查。进入第一层检查后,让各个线程竞争锁,竞争到锁的线程(A)来进行第二层检查。
第二层检查:
线程A获得锁后,再次判断,如果实例仍然未被创建,则创建实例。如果实例已经被创建,那么就是上次拿到锁的线程(B)创建了实例(B创建的时候A被阻塞在获得锁的地方,A并不知道B先于它获得了锁,如果不再次判断,那么仍然有可能创建多个实例),返回实例。
懒汉模式的优点:
1.第一次要是用单例的时候才去创建,程序启动的时候无负载。
2.多个里启动顺序可以再由控制。
懒汉模式额缺点:
1.代码复杂
2.需要考虑线程安全,由用户加锁。