[C++系列] 43. 懒汉模式剖析----单例模式

懒汉模式:

如果单例对象构造十分耗时或者占用很多资源,比如加载插件, 初始化网络连接,读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。

首先,有一个极其简单的单线程懒汉模式,初步理解它的思想:

// 单线程懒汉模式
class Singleton
{
public:
	static Singleton* getInstance() {
		// 做饭
		// 提高后续线程调用接口的效率
		if (_sin == nullptr) {       // 第一次为空,创建对象,第二次非空,直接返回,保证单例
			_sin = new Singleton;
		}
		return _sin;
	}
private:
	// 1. 构造函数私有化   2. 拷贝构造私有化(不必实现)   3. 赋值运算符无所谓私有化,因为其不创建新的对象
	Singleton(){}       
	Singleton(const Singleton& s) = delete;


	// static Singleton _sin;      // 启动之前肯定需要进行初始化,东西已经准备好了,为饿汉模式
	static Singleton* _sin;        // 定义为指针,与对象不为同一类型,其为单独的指针类型
};

Singleton* Singleton::_sin = nullptr;

进程、单线程、多线程

一个可执行的程序就是一个进程,在一个进程内可创建多个执行流,也称为线程。即在进程内只有一个执行流称为单线程程序。相当于原来是一个人干事情,变成很多人干同一件事情,单进程是一个串行流。例如:双11、双12,开启秒杀活动时,排队进行串行流可能连页面都进不去,然而现实中并没有这么夸张。

实际上,这里面都是些高并发的操作,涉及到多线程操作,你的请求不是一个线程去执行,而是多个或者一批线程去执行,提高了总体的效率,相当于一开始只有一个人处理你的请求,现在一批人帮着去处理。如生活中常见的是银行柜台,以前可能只有一个柜台,需要排队且队伍很长,现在有多个柜台,但也需要排队但队伍很短也就是提高了效率。                                                 即有多个执行流时,会大大提高效率。每一个进程都有一个进程地址空间,且不共享,即不同的进程有不同的进程地址空间。但一个进程有多个线程的话,线程会共享该进程的地址空间,它们之间访问的内存可能是同一片内存。相当于出现你和很多人同时在修改一篇文章,预测值与实际值大相径庭。

int main() {
	int i = 0;
	i++;        // 针对 ++ 指令
	return 0;
}

//i++;
//009F32CF  mov         eax, dword ptr[i]
//009F32D2  add         eax, 1
//009F32D5  mov         dword ptr[i], eax

对于++指令来讲,若有两个线程同时执行该指令,期望值为2,但是它仍可能是1,。在此其实为“同时写”的问题。其本来为“串行”的操作,执行两次。                                                                                                                                                                    在此引入“原子操作”,对于最小的执行指令的单位,可以理解为一条指令,其不会在CPU中被打乱,即说的一句话不会被打乱,但多句话可能会被打乱。                                                                                                                                                                下面是针对i++的反汇编,虽然只有一行代码,但在底层实现上其有三步操作。假如有两个线程去改变该值,eax是寄存器,假如为多核的寄存器,每一个核上均有一个线程去执行,每一个CPU都有一个寄存器,两个寄存器对两个核进行一个++的操作,那么两个核就有两个串。第一个线程将i放入寄存器中,现在为0,紧接着第二个线程再去拿这个值,i依旧还是0,第一个线程执行完之后将1写入i中,但是当第二个线程执行完毕后,又将1写入i中,导致i最后结果为1。但是也有可能为2,即第一个线程执行完毕后,第二个线程再进行执行。但是这一切全靠CPU的时间分配,CPU实际上是一个“笼形”的操作,线程之间无法确定先后,会产生线程安全问题,写的时候可能会写错,不会达到预期的效果。而饿汉模式没有线程安全的问题,因为其“只读”,不对其进行修改。但是“懒汉模式”因为它为创建为主,可能会多个线程创建出多个对象。

void fun() {
	for (int i = 0; i < 10; i++) {
		cout << i << endl;
	}
}

#include <thread>            // C++11 加入线程库,C++98没有   <pthread> Linux中的系统库,两者均可以使用线程

int main() {
	int i = 0;
	thread t1(fun);      // main为主线程 
	thread t2(fun);	     // 创建了t1、t2两个线程

	t1.join();	     // 相当于 会和 的意思,让主线程等待上面两个线程执行完自己的命令一起结束
	t2.join();

	system("pause");
	return 0;
}

对于多线程操作,其是没有一定的顺序的,是一种随机的操作。

当两个线程执行到getlnstance有三种情况,t1先到、t2先到、同时到。当为同时到时无法判断哪个对象是谁new出来的,那么返回对象的地址也不知道是哪个创造出来的,即无法保证“单例”前提,后创建的对象很可能覆盖掉前一个的地址,导致前一个对象内存丢失极易造成内存泄露,危害极大。

void testsingleton() {
	cout << Singleton::getInstance() << endl;     // 测试双线程调用getInstance创建对象的地址
}

int main() { 
	thread t1(testsingleton);                     // 建立两个线程
	thread t2(testsingleton);

	system("pause");
	return 0;
}

#include <mutex>                  // 加锁头文件,互斥锁,所有的线程共用同一把锁,全局只有一把锁,用一把锁限制所有线程
private:
    static mutex _mtx;	          // 全局只有一把锁,限制全部线程

static Singleton* getInstance() {
    _mtx.lock();                  // 加锁不能在if内,没有意义,还是要创建对象
    if (_sin == nullptr) {        // 第一次为空,创建对象,第二次非空,直接返回,保证单例
        _sin = new Singleton;
    }
    _mtx.unlock();
}

请仔细思考一下:这样加锁的方式是能够保证对象创建单一,不会造成内存泄露。但是,每一个线程进来之后都得被锁住,再判断,再解锁,效率大大降低。加锁是为了创建一次对象,对象创建好之后就不需要走判断逻辑了,下一次线程进来之后直接返回地址即可,可以大幅度提高效率。

static Singleton* getInstance() {
    if (_sin == nullptr) {
        _mtx.lock();                 // 加锁不能在if内,没有意义,还是要创建对象
        if (_sin == nullptr) {       // 第一次为空,创建对象,第二次非空,直接返回,保证单例
	_sin = new Singleton;
        }
    _mtx.unlock();
    }
    return _sin;
}

 在调用getInstance时,用new申请了空间,但用完我们并没有释放空间。现在,也不需要手动去释放,单例不仅仅在一个地方使用,可能也在其它地方使用,释放了会导致程序崩溃。

Singleton* ps = Singleton::GetInstance();
delete ps;
ps = nullptr;

所以我们只能delete ps,再将ps置空,但是,将ps空间释放之后,类中的空间又没有被释放,还是一个有效值。而且不光在此使用这个空间,在其他的地方到该空间的接口,一开始调用的时候该指针有效,但在此已经被释放了,会出现解引用的错误。不能手动去释放。

在此,一般可以不用管,因为其为静态成员,在整个程序运行周期内均有效,程序运行结束即进程结束,那么会将所有的空间资源均返还给系统,达到垃圾回收的目的。

但是若是想手动释放的话,可以在内部定义一个内部类辅助操作。内部类可以访问外部类的私有成员,并且可以直接访问。

class Singleton
{
public:
	static Singleton* getInstance() {
		// 做饭
		// 提高后续线程调用接口的效率
		if (_sin == nullptr) {
			_mtx.lock();                 // 加锁不能在if内,没有意义,还是要创建对象
			if (_sin == nullptr) {       // 第一次为空,创建对象,第二次非空,直接返回,保证单例
				_sin = new Singleton;
			}
			_mtx.unlock();
		}
		return _sin;
	}
	class GC {                                   // 定义内部类,进行垃圾回收
	public:
		~GC() {
			if (_sin) {
				delete _sin;
				_sin = nullptr;
			}
		}
	};
private:
	// 1. 构造函数私有化   2. 拷贝构造私有化(不必实现)   3. 赋值运算符无所谓私有化,因为其不创建新的对象
	Singleton(){}       
	Singleton(const Singleton& s) = delete;


	// static Singleton _sin;      // 启动之前肯定需要进行初始化,东西已经准备好了,为饿汉模式
	static Singleton* _sin;        // 定义为指针,与对象不为同一类型,其为单独的指针类型
	static mutex _mtx;			   // 全局只有一把锁,限制全部线程
	static GC _gc;
};

Singleton* Singleton::_sin = nullptr;
mutex Singleton::_mtx;
Singleton::GC Singleton::_gc;          // 它是静态成员,其生命周期也是整个程序的生命周期,调用析构函数释放空间

为什么要采用内部类来做这样一个事情呢?为什么不能在单例上直接写析构函数进行资源回收呢?

class Singleton
{
public:
	static Singleton* getInstance() {
		// 做饭
		// 提高后续线程调用接口的效率
		if (_sin == nullptr) {
			_mtx.lock();                 // 加锁不能在if内,没有意义,还是要创建对象
			if (_sin == nullptr) {       // 第一次为空,创建对象,第二次非空,直接返回,保证单例
				_sin = new Singleton;
			}
			_mtx.unlock();
		}
		return _sin;
	}
	~Singleton() {                               // 单例中析构函数产生递归效果
		if (_sin) {                          // 之前所产生的对象不为当前类,不会重复递归调用析构函数
			delete _sin;
			_sin = nullptr;
		}
	}

~Singleton,会在delete _sin上重复调用析构函数产生递归效应。因为之前调用析构函数释放的资源不是当前类类型的,不会去递归调用当前类的析构函数,而再次刚好触发了该条件。

懒汉模式完整版:

// 懒汉
// 优点:第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制。
// 缺点:复杂

// 五大实现要点
// 1. 构造函数私有
// 2. 封死拷贝构造
// 3. 提供静态线程安全的接口(double-check,提高效率)
// 4. 定义一个静态单例类型的指针,初始化为nullptr
// 5.(可选)定义一个内部类,辅助释放单例指针
#include <iostream>
#include <mutex>
#include <thread>      // C++11中的线程库   <pthread> Linux下线程...
using namespace std;

class Singleton
{
public:
	static Singleton* GetInstance() {
		// 注意这里一定要使用Double-Check的方式加锁,才能保证效率和线程安全
		if (nullptr == m_pInstance) {
			m_mtx.lock();          // 加锁在if外部,注意是否为有效锁
			if (nullptr == m_pInstance) {
				m_pInstance = new Singleton();
			}
			m_mtx.unlock();
		}
		return m_pInstance;
	}

	// 建立内部类,实现一个内嵌垃圾回收类    
	class CGarbo {
	public:
		~CGarbo() {
			if (Singleton::m_pInstance)
				delete Singleton::m_pInstance;
		}
	};

	// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
	static CGarbo Garbo;

private:
	// 构造函数私有
	Singleton() {};

	// 防拷贝
	Singleton(Singleton const&);
	Singleton& operator=(Singleton const&);

	static Singleton* m_pInstance; // 单例对象指针
	static mutex m_mtx;            //互斥锁
};

Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Garbo;
mutex Singleton::m_mtx;

void func(int n)
{
	cout << Singleton::GetInstance() << endl;
}

// 多线程环境下演示上面GetInstance()加锁和不加锁的区别。
int main()
{
	thread t1(func, 10);
	thread t2(func, 10);

	t1.join();
	t2.join();

	cout << Singleton::GetInstance() << endl;
	cout << Singleton::GetInstance() << endl;
	system("pause");
}

void MemoryLeaks()
{
	// 1.内存申请了忘记释放
	// 理论上申请了内存没有进行释放导致内存泄露,实际上程序运行完之后,
	// 栈会将所有资源全部还给系统
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;

	// 2.异常安全问题
	int* p3 = new int[10];

	Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.

	delete[] p3;
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值