超详细的单例模式详解

目录

什么是单例模式

饿汉单例模式

懒汉单例模式

关于单例模式引发的几点思考


什么是单例模式

单例模式是什么?根据其名字而言,单例说明该类只有一个实例,那么要访问这一个唯一的实例,则需要提供一个接口来公共访问该实例。对于实例的创建时期不同,单例模式又分为饿汉单例模式以及懒汉单例模式,下面则分别进行介绍。

饿汉单例模式

试想一下对于一个十分饥饿的人而言,他是愿意获取线程的食物还是自己制作事物呢?我相信答案肯定是直接获取事物。而饿汉单例模式顾名思义该实例就是在调用接口之前已经创建好了的,用的时候直接使用即可。

下面给出实现方式:

class Singleton {
public:
	static Singleton* getInstance() {    //提供访问该对象的方法
		return object;
	}
private:
	Singleton() {}     //构造函数私有化
	static Singleton* object;   //创建唯一的一个对象
};
Singleton* Singleton::object = new Singleton();

         饿汉式单例模式一定是线程安全的吗???

         答案是确定的,饿汉式单例模式一定是线程安全的,因为在程序调用前,实例便已经创建完成了。不存在在创建实例的过程中发生多线程的竞争条件。

懒汉式单例模式

想必都听说过“临时抱佛脚”这句话吧,老师布置的作业未能及时完成,在老师说要收的时候才开始急急忙忙的写作业。懒汉式单例模式就是这样的,对象并不是在使用之前就已经创建好,而是要使用的时候才进行创建。

下面给出实现方式:

class Singleton {
public:
	static Singleton* getInstance() {    //提供访问该对象的方法
		if (object == nullptr) {
			object = new Singleton();        //当程序运行到这里的时候才开始创建对象
		}
		return object;
	}
private:
	Singleton() {}     //构造函数私有化
	static Singleton* object;   //创建唯一的一个对象
};
Singleton* Singleton::object = nullptr;

         思考一个问题,上面所给出的代码是线程安全的吗???

         答案是否定的。假设Singleton类刚刚被初始化,instance对象还是空,这时候两个线程同时访问getInstance方法,那么object对象将被创建两次,显然是错误的。下面通过使用互斥锁对代码进行修改:

std::mutex mx;
class Singleton {
public:
	static Singleton* getInstance() {    //提供访问该对象的方法
		if (object == nullptr) {
			unique_lock <std::mutex> lock(mx);
			if (object == nullptr) {     //锁加双重判断
				object = new Singleton();        //当程序运行到这里的时候才开始创建对象
			}
		}
		return object;
	}
private:
	Singleton() {}     //构造函数私有化
	static Singleton* object;   //创建唯一的一个对象
};
Singleton* Singleton::object = nullptr;

         再来分析这个问题,假设Singleton类刚刚被初始化,instance对象还是空,这时候两个线程A和B均访问getInstance方法,假设A线程获取锁,那么B线程将发生阻塞,当A线程创建对象完成后,释放锁,此时B对象进入第二次if判断,发现不等于nullptr,则直接返回。

         再次思考一个问题,上面实现的懒汉单例模式一定正确吗??? 

答案是否定的。利用new生成一个对象实际上会经过以下三步:

        1、开辟一段内存空间

        2、对象的初始化操作

        3、设置对象指向这块内存的地址

但如果编译器进行了一些优化,使之顺序编程了1,3,2,那么线程一在执行第二步的时候,对象地址不为nullptr,线程二直接放回一个未初始化的空对象,造成了不安全的情况。

解决办法:

可以通过volatile关键字来保证,volatile的作用由两个:1.保证可见性,2.防止执行重排序。为singleton加上volatile即可:

volatile static Singleton *object;

对于借助互斥锁实现单例模式,还可以通过一下方式去设计。实现方式如下:

class Singleton {
public:
	static Singleton* getInstance() {    //提供访问该对象的方法
		static Singleton object;
		return &object;
	}
private:
	Singleton() {}     //构造函数私有化
};

上面的单例模式在多线程环境中使用时,会不会出现这种情况,线程A第一次调用getInstance函数的时候,single对象第一次初始化,此时线程B也调用getInstance函数,会不会也进行single对象的初始化呢,因为此时线程A并没有初始化完single?

在Linux环境中,通过g++编译上面的代码,命令如下:
g++ -o main main.cpp -g
生成可执行文件main,用gdb进行调试,到getInstance函数,并打印该函数的汇编指令,如下:

在这里插入图片描述

可以看到,对于static静态局部变量的初始化,编译器会自动对它的初始化进行加锁和解锁控制,使静态局部变量的初始化成为线程安全的操作,不用担心多个线程都会初始化静态局部变量,因此上面的懒汉单例模式是线程安全的单例模式!

关于单例模式引发的几点思考

 1、单例模式中获取对象的方法为什么设计为静态的,一定要是静态的吗???

        关于这个问题首先我们来看一下关于单例模式的设计步骤:

        1、将构造函数私有化,目的是防止其他程序创建对象。

        2、在类中创建唯一的一个实例。

        3、提供一个方法供外部访问该对象。

成员方法的访问方式只能通过两种方法访问。其一是通过对象访问,由于外部不能创建对象,所以这种方式不可行;那么第二种方法就是通过类名访问了,而通过类名访问的成员方法必须是静态成员方法,因为静态成员方法不依赖对象,不需要传入this指针。而静态成员方法访问的成员变量也必须是静态成员变量。因此创建的实例以及提供访问实例的接口都被设计为静态的。

2、既然饿汉式单例模式一定是线程安全的,那么为什么还借助互斥锁等手段去设计懒汉式单例模式呢?这不是浪费资源吗???

这种说法是错误的,饿汉式单例模式一定是线程安全的,这是它的优点所在,但是人无完人,在优点的背后,也隐藏了一些缺点,比如说我设计了饿汉式单例模式,一开始我就创建好了对象,但是自始至终我都没有访问过该对象,那么这无疑是对系统资源的浪费,即空间换时间的做法。而对于懒汉式单例模式呢?它并不存在资源浪费的情况,因为他是在需要的时候才进行对象的创建,但是对于懒汉式单例模式相比饿汉单例模式所需花费的时间更长,即时间换空间的做法。

3、单例模式的应用场景是什么?什么时候应该运用单例模式???

 1.需要生成唯一序列的环境

 2.需要频繁实例化然后销毁的对象。

 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 

4.方便资源相互通信的环境

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值