设计模式之单例模式

定义

单例模式:保证一个类仅有一个实例,并提供一个访问他的全局访问点。
单例模式分为:
①懒汉模式:在第一次调用时实例化
②饿汉模式:在创建类的时候实例化

优点:在内存中只有一个实例,减少内存开销、全局统一的访问点可以严格控制对象的访问

缺点:没有接口,扩展困难

应用场景:数据库连接池的设计

实现步骤:
	①构造函数私有化
	②提供一个全局的静态方法(全局访问点)
	③在类中定义一个静态指针,指向本类唯一的实例对象

懒汉模式单例(不保证线程安全)

c++代码

#include <iostream>

using namespace std;

class SingleLenton
{
public:
	static SingleLenton* getInstance()
	{
		//对外接口中判断指向本类唯一实例的指针是否为空
		if (!instance)
		{
			//为空说明该类还没有实例化对象
			cout << "实例化对象, 即将调用私有的构造函数。\n";
			instance = new SingleLenton();
		}
		//不为空说明该类已存在实例化对象,返回其指针即可
		return instance;
	}

private:
	//构造函数私有化 
	SingleLenton();		//声明未定义-未实现,也可内联实现SingleLenton() { cout << "只会被调用一次的私有构造函数\n"; }
	//提供一个静态指针,指向本类唯一实例。
	//问题:为什么要静态的
	static SingleLenton* instance;
};

//私有构造函数实现
SingleLenton::SingleLenton()
{
	cout << "只会被调用一次的私有构造函数\n";
}

//私有静态成员初始化
SingleLenton* SingleLenton::instance = nullptr;


int main()
{
	//懒汉单例
	//调用静态方法:①可通过实例对象调用	②也可用 类名::函数名 调用
	SingleLenton* pOne = SingleLenton::getInstance();	//第一次调用私有构造
	cout << "测试:私有构造在哪被调用\n";
	SingleLenton* pTwo = SingleLenton::getInstance();	//没有调用私有构造
	if (pOne == pTwo)
	{
		//判断两指针是否指向同一对象
		cout << "指向同一实例对象,单例实现成功!\n";
	}

	
	getchar();
	return 0;
}

问题1:为什么instance 和 getInstance() 要声明为static 的?

补充:用static修饰的成员称为“静态成员”。这种数据成员的生存期是大于类的实例对象的。和普通数据成员对比:静态数据成员是每个类的
有一份(即属于类),普通数据成员是每个对象有一份,因此静态成员变量也叫做类变量,普通成员变量叫做实例变量。用static修饰的函数
叫做静态成员函数,静态成员函数属于类,不属于某一个实例对象(生存期大于实例对象)。

根据上面单例的实现过程,可通俗理解为:你不能用该类在其他地方创建对象,而是通过该类自身提供的方法访问类中的那个自定义对象。
那么问题的关键来了,程序调用类中方法只有两种方式,①创建类的一个对象,用该对象去调用类中方法;②使用类名直接调用类中方法,格式“类名::方法名()”;
构造函数私有化后第一种情况就不能用(通过类自身提供的方法得到示例对象,但又需要实例对象调用方法。矛盾了),只能使用第二种方法。
而使用类名直接调用类中方法,类中方法必须是静态的,而静态方法不能访问非静态成员变量,因此类自定义的实例变量也必须是静态的。
这就是单例模式唯一实例必须设置为静态的原因。

问题2:为什么懒汉模式的单例不保证线程安全?

假设存在这样一种并发情况:
有两个线程A 和 B,创建时均已SingleLenton::getInstance() 函数作为线程函数,假设第一个线程A 首先进入SingleLenton::getInstance() 函数,此时对线程A 来说static SingleLenton* instance = nullptr,即还没有实例化对象,于是进入if 语句块,不幸的是这时线程A 的cpu调度时间到了,还没有生成实例对象时(即还没执行new 语句)。

第二个线程B 开始运行,进入线程函数SingleLenton::getInstance(),发现此时instance也是空的(线程A 还没有执行new 语句),幸运的是线程B 的cpu调度时间未到,于是继续执行,进入if 语句块生成一个实例对象。之后线程B 的cpu时间调用到了。

再次回到线程A 上次结束的语句(进入了if 语句块,有可能是cout语句),于是线程A 有生成了一个实例对象。

至此,类SingleLenton存在两个实例对象,这违背了单例的定义,因此说懒汉模式的单例不保证线程安全。懒汉模式的单例不保证线程安全 可以理解成 【任何可能违背设计者意图的方式,都是“不安全”的。】

测试结果

不安全的懒汉模式

实现线程安全的懒汉模式单例

如何实现

doublecheck(二次检查)+ 锁。

c++代码

#include <iostream>
#include <mutex>

using namespace std;

mutex mt;
class SingleLenton
{
public:
	static SingleLenton* getInstance()
	{
		if (!instance)	//第一次检查
		{
			//为空再加锁,通过lock_guard 类管理互斥量
			lock_guard<mutex> lg(mt);
			if (!instance)	//二次检查
			{
				cout << "实例化对象, 即将调用私有的构造函数。\n";
				instance = new SingleLenton();
			}
		}

		return instance;
	}

private:
	SingleLenton() { cout << "只会被调用一次的私有构造函数\n"; }
	//删除拷贝构造
	SingleLenton(const SingleLenton& s) = delete;
	//删除=运算符重载
	SingleLenton& operator= (const SingleLenton& s) = delete;
	//静态成员变量一定要初始化
	static SingleLenton* instance;
	
};
SingleLenton* SingleLenton::instance = nullptr;

int main()
{
	//懒汉单例
	//调用静态方法:①可通过实例对象调用	②也可用 类名::函数名 调用
	SingleLenton* pOne = SingleLenton::getInstance();	//第一次调用私有构造
	cout << "测试:私有构造在哪被调用\n";
	SingleLenton* pTwo = SingleLenton::getInstance();	//没有调用私有构造
	if (pOne == pTwo)
	{
		//判断两指针是否指向同一对象
		cout << "指向同一实例对象,单例实现成功!\n";
	}

	
	getchar();
	return 0;
}

为什么安全?
假设还是上面那个情景,线程A 进入第一个if 语句块但未执行到加锁语句,此时线程A 的时间调度完了。

线程B 开始运行,也进入第一个if 语句块,因为线程A 未加锁,所以这里线程B 上锁并准备第二次检查,此时线程B 的时间调度完了。

回到线程A ,线程A (进入第一个if 语句块了)准备加锁,发现已上锁,于是等待解锁,因为线程B 中的锁一直未释放,所以线程A 这次的时间调度会一直等待。

第二次进入线程B ,已上锁且线程A 未生成实例对象,所以线程B 顺利通过第二次检查并成功生成了实例对象,生成完毕后解锁互斥量。

再次回到线程A 中,顺利上锁,但第二次检查时发现instance已不为空(说明实例对象已存在),于是线程A 会解锁互斥量并退出if 语句块,然后返回已生成(线程B 中生成)的实例对象指针。

测试结果

实现线程安全的懒汉模式

饿汉模式单例

保证线程安全:即任何情况下只有一个实例存在
和懒汉的区别主要在于实例对象创建的时机。饿汉在创建类时就创建了实例化对象

#include <iostream>

using namespace std;

class SingleLenton
{
public:
	static SingleLenton* getInstance()
	{
		cout << "实例对象已存在,无需再次创建\n";
		return instance;
	}
private:
	//构造函数私有化
	SingleLenton() { cout << "只会被调用一次的私有构造函数\n"; }
	//删除拷贝构造
	SingleLenton(const SingleLenton& s) = delete;
	//删除=运算符重载
	SingleLenton& operator= (const SingleLenton& s) = delete;

	//一个静态的指针
	static SingleLenton* instance;	//创建时就生成实例对象
};

//初始化静态指针,即生成实例对象
SingleLenton* SingleLenton::instance = new SingleLenton();

int main()
{
	//懒汉单例
	//调用静态方法:①可通过实例对象调用	②也可用 类名::函数名 调用
	SingleLenton* pOne = SingleLenton::getInstance();	//第一次调用私有构造
	cout << "测试:私有构造在哪被调用\n";
	SingleLenton* pTwo = SingleLenton::getInstance();	//没有调用私有构造
	if (pOne == pTwo)
	{
		//判断两指针是否指向同一对象
		cout << "指向同一实例对象,单例实现成功!\n";
	}

	
	getchar();
	return 0;
}

测试结果

饿汉模式单例

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值