单例设计模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例;
简单来说就是单例类只能实例化出一个对象;概念不难理解,我们要探讨的就是如何实现单例类,也就是如何控制一个类才能保证只有一个类呢?
单例分为饿汉模式和懒汉模式
我们回顾一下之前的创建一个类的方式;
-->调用构造函数对吧,而且是public权限的构造函数;
诶?那我们如果把构造函数设置为私有的了,那么类外这不就无法创建对象了吗,确实是起到了限制对象数量的作用;
但这样我们该如何创建对象呢?
-->类外无法创建那就在类内创建对象!因为单例本就是只有一个对象,该对象会被其他线程共享,数据都是一致的,所以我们只需要维护好这一个类的方法就好了,在哪里创建不关心这个;
我们在类内创建一个对象;
class A
{
private:
A()
{}
private:
A _ins;
public:
void f()
{
cout << "创建一个单例对象" << endl;
}
};
但是这里会出现问题,那么就A类还没有构建完毕,是不可以使用A类型作为成员的;
那我们应该如何定义呢?--->静态成员变量;
静态成员变量是比较特殊的成员变量,他处于全局区,每一个类只能实例化出的里面的静态成员都是同一个;
需要注意在类外初始化;正是因为他是在类外实例化的,所以不会和A类同时构造,而是实例化的时间不同;不会出现上述报错;
class A
{
private:
A()
{}
private:
static A _ins;
public:
void f()
{
cout << "创建一个单例对象" << endl;
}
static A GetA()
{
return _ins;
}
};
所以我们现在已经可以通过类内创建A对象。并且也是可以直接通过域名直接获得,但是为了体现封装性最好使用一个接口;当然这个接口也是要使用static,因为我们要使用域名访问该接口,然后获得单例对象;
饿汉式在类加载时就立即创建单例对象,不管这个对象是否马上会被使用。就好像一个 “饥饿” 的人,迫不及待地在一开始就把东西准备好。
映射到代码上就是我先创建好一个对象,但是可能没有被使用,一等到有用户要用,立马给用户,这中间没有多余的动作;
#include<bits/stdc++.h>
#include<mutex>
using namespace std;
namespace hunger_instance {
//饿汉模式
class Instance
{
public:
static Instance* GetInstance()
{
return &_ins;
}
void f()
{
cout << "获取到了一个单例对象..." << endl;
}
private:
Instance(const Instance&) = delete;
Instance() = default;
private:
static Instance _ins;
};
Instance Instance::_ins;
}
代码中可以很好的体现出饿汉性质,_ins先初始化好了,用户可以直接通过域名调用GetInstance()获得单例对象地址;
懒汉核心特点是在需要使用单例对象时才进行创建,即延迟加载。不会像饿汉方式一样,先把对象创建好,用的时候直接拿,懒汉不会先创建好对象,而是需要时才创建;
namespace lazy_instance
{
std::mutex mtx;
//饿汉模式
class Instance
{
public:
static Instance* GetInstance()
{
if (_ins == nullptr)
{
{
std::lock_guard<std::mutex>lock(mtx);
if (_ins == nullptr)
{
_ins = new Instance();
}
}
}
return _ins;
}
void f()
{
cout << "获取到了一个单例对象..." << endl;
}
private:
Instance(const Instance&) = delete;
Instance() = default;
private:
static Instance* _ins;
};
Instance* Instance::_ins = nullptr;
}
由于不是在类外先创建好的,所以我们不能使用普通对象,以为普通对象类外一初始化就是实例化了;
所以懒汉模式的对象是指针;类外初始化为空指针;
与饿汉模式另一点不同的是GetInstance方法,这里因为会存在线程安全所以需要用到互斥锁,以及双层循环保证只new一个对象;
这里我以3个线程为例,分别为1、2、3号线程,同时要获取单例对象;
假设是1号线程先拿到了锁,还没有new呢,对象这个时候还是nullptr,但是这个时候2号线程也通过了第一层判断进来了,阻塞在了锁处,因为拿不到锁;
然后1号线程创建完对象,释放锁给2号线程,2号线程进来后,需要看一看对象是不是已经创建好了,2号对象发现对象指针不为空,说明已经创建好了,所以什么也不用干释放锁;也可以拿到对象指针;
3号线程这时候来了,在第一层判断处,他就发现指针不为空,那我直接return _ins就好了;
总结:双层判断是必要的,第一个判断很简单,就是避免进入该方法的线程避免在指针不为空的情况下还new;
第二层循环配合锁是对象还没创建完时,避免进入第一层循环内部的多个线程多次new对象;
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有