单例模式

单例模式--保证一个类仅有一个实例,并提供一个访问他的全局访问点,该实例被程序所有模块共享。

单例模式结构图

 

特点

  • 通常我们可以让一个全局变量使得一个对象被访问,但不能防止被实例化多个对象。最好方法就是让类自身负责保存它的唯一实例,这个类可以保证没有其他实例可以被创建,并且提供一个访问该实例的方法。
  • 单例模式下,有Singleton类封装其唯一实例,这样它可以严格控制客户端怎样访问以及何时访问。简单来说就是对唯一实例的受控访问。

根据处理方式的不同,单例模式可分为:饿汉模式懒汉模式

饿汉模式:

饿汉模式是通过静态初始化方式,在程序被加载时就实例化创建唯一对象。

class Singleton{
public:
	static Singleton& GetSingleton() // 不能传值,只能传指针或者引用
	{
		cout << "Creat Singleton Object;" << endl;
		return pSing;
	}

private:
	Singleton(){}
	Singleton(const Singleton& pS) = delete;
	static Singleton pSing;
};

Singleton Singleton::pSing;
int main()
{
	Singleton::GetSingleton();
	
	return 0;
}

由于私有化了构造函数、禁用了拷贝构造,所以我们只能通过专门的接口Singleton::GetSingleton()来调用唯一对象。

 

懒汉模式:

懒汉模式是在被调用时才实例化创建唯一对象。

class Singleton{
public:
	static Singleton* GetSingleton() 
	{
		if (pSing == nullptr)
			pSing = new Singleton();
		
		cout << "Creat Singleton Object;" << endl;
		return pSing;
	}

	static void DestorySingleton(){
		delete pSing;
		pSing = nullptr;
		cout << "Destory Success;" << endl;
	}

private:
	Singleton(){}
	Singleton(const Singleton& pS) = delete;
	static Singleton* pSing;
};

Singleton* Singleton::pSing = nullptr;
int main()
{
	Singleton::GetSingleton();
	Singleton::DestorySingleton();

	return 0;
}

 

多线程时的单例:

由于饿汉模式下的单例是静态初始化,在程序运行时唯一对象已经创建好,没有在多个线程访问的安全问题。

但懒汉模式下,可能出现多个线程同时访问Singleton类,调用GetSingleton()方法,可能会创建多个实例。

对于处理线程安全问题上,我们可以通过互斥锁来保证:当一个线程位于代码临界区时,另一个线程不进入临界区,如果其他线程试图进入锁住的代码,它会被一直阻塞,直到该对象被释放

/*
   所以创建互斥锁
   static mutex m;
*/

static Singleton* GetSingleton() 
{
	m.lock();
	if (pSing == nullptr)
		pSing = new Singleton();

	cout << "Creat Singleton Object;" << endl;

	m.unlock();

	return pSing;
}

但是单纯像上面所展示的代码一样,直接加锁,那么每次有线程调用GetSingleton()时,都需要加锁、解锁,而如果已经实例化了对象这样的操作无疑是影响性能的。

改进方式:先进行一次判断,如果已经实例化了对象则,直接返回,不需要等待。

static Singleton* GetSingleton() 
{
	if (pSing == nullptr){
		m.lock();
		if (pSing == nullptr)
			pSing = new Singleton();

		cout << "Creat Singleton Object;" << endl;

		m.unlock();
	}
	return pSing;
}

现在,我们就不用让线程每次都加锁,而只是在实例未被创建时再加锁处理,既提高了效率,也保证了线程安全。这种做法也叫做双重锁定

 

指令重排:

指令重排是一种编译器优化方式,通过调整指令执行顺序,来提高性能。

我们在new创建对象时,通常是:

1. 调用operator new,创建内存空间

2. 调用构造函数,初始化对象 

3. 将对象的地址赋值给指针变量

在第2步 初始化对象是一个内存操作,这可能是一个耗时操作,为了避免CPU在等待这个操作时停顿导致性能下降,编译器会调整指令顺序(开启优化)或者有些架构的CPU会乱序执行指令,也就是将 3 提前执行,这样的话,指针就指向了一个未初始化好的对象。外部得到的单例对象是一个未初始化好的对象,就会引发问题。

考虑到这点,我们可以通过 volatile关键字声明pSing。

static volatile Singleton* pSing;

volatile 作用:

  1.  保证内存可见性。(在多线程环境中,任何线程对共享变量的修改,其他线程都是可感知的。)
  2.  防止指令重排。

 

总结:

饿汉模式与懒汉模式

      饿汉模式:是静态初始化方式,在自己被加载是就将自己实例化;

由于是静态初始化方式,所以程序一加载就需要实例化对象,提前占用系统资源。多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。

但可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。

      懒汉模式:在第一次被引用时,才会将自己实例化。

与饿汉模式不同,是一种延迟启动方式,所以程序启动较快。

但由于调用时才实例化,在多线程下存在线程安全问题,需要双重锁定才能保证安全,实现上比较麻烦。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值