一、单例模式概述
在一个项目中,全局范围内,某个类的实例有且只有一个,通过这个实例向其他模块提供数据的全局访问这种模式叫做单例模式,单例模式的典型应用是任务队列。单例的对象在整个项目中充当着全局变量的角色。
二、为什么使用单例代替全局变量?
直接使用全局变量他会破坏类的封装,在每个模块都可以对这个全局变量进行修改,可以理解为恶意篡改,那么如果一个模块给他修改了,其他模块就会受到影响,得不到正确的值了。使用单例模式这个全局变量就会被封装在这个类里面,并且作为单例类的私有成员变量,不能被访问,我们需要在类里提供一个访问私有成员变量的方法,也就是处理函数,外部都要按照规定访问这个私有成员变量,不按照规定就不要访问。
三、单例模式代码
cpp
#include <iostream>
using namespace std;
class TaskQueue
{
public:
TaskQueue(const TaskQueue &t) = delete;
TaskQueue& operator = (const TaskQueue &t) = delete;
static TaskQueue* getInstance()
{
return m_taskQ;
}
void print()
{
cout << "我是单例对象里面的成员函数!" << endl;
}
private:
TaskQueue() = default;
//只能通过类名访问静态属性方法
static TaskQueue* m_taskQ;
};
//类外初始化
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;
int main()
{
TaskQueue* taskQ = TaskQueue::getInstance();
taskQ->print();
return 0;
}
代码详解及思路:
一个类在实例化一个唯一的对象(类的实例对象只能创建一个)
首先定义一个单例模式的类class TaskQueue{};
在创建完类之后默认自带三个构造函数,一个析构,两个操作符重载。
一个是不含参的构造函数(可以构造新的对象实例)
一个是拷贝构造函数(我有一个已经存在对象的实例就能构造新的对象实例)
一个是移动构造函数(不可以创建新的对象实例)
一个析构函数
一个拷贝赋值操作符重载
一个移动赋值操作符重载
我们要处理不含参数的构造函数 和拷贝构造函数 ,因为这两个都可以创建新的对象实例和我们单例模式只有唯一的实例相冲突。把这两个构造函数设置成私有的=default或者delete,如上图代码。
现在我们想在类中创建对象只能通过类名创建对象 ,对象一定是静态的对象,通过类名访问类内的属性和方法一定是静态的,如果不是静态的一般都是通过对象调用的,现在已经把在类外部创建对象的路都堵死了,所以得到对象一定要通过类名!
所以定义一个静态成员变量(静态的taskQ指针)、还要定义静态成员函数用来操作他,可以得到对应的实例也就是静态的m_taskQ,在类外初始化,主函数通过静态方法获得单例对象被taskQ指针保存起来了,通过这个指针调用单例里面唯一的方法,使用者就可以调用类里的成员函数了。
以上是饿汉模式---定义类的时候创建对象代码。
相应的还有一种懒汉模式:
以下是懒汉模式---什么时候使用这个单例对象,在使用的时候再去创建对应的实例。(具体变动比较以下就可以知道)。
cpp
class TaskQueue
{
public:
TaskQueue(const TaskQueue &t) = delete;
TaskQueue& operator = (const TaskQueue &t) = delete;
static TaskQueue* getInstance()
{
if (m_taskQ == nullptr)
{
m_taskQ = new TaskQueue;
}
return m_taskQ;
}
void print()
{
cout << "我是单例对象里面的成员函数!" << endl;
}
private:
TaskQueue() = default;
//只能通过类名访问静态属性方法
static TaskQueue* m_taskQ;
};
//类外初始化
TaskQueue* TaskQueue::m_taskQ = nullptr;
int main()
{
TaskQueue* taskQ = TaskQueue::getInstance();
taskQ->print();
return 0;
}
懒汉模式比较饿汉模式节省内存空间,饿汉模式当我不需要这个实例对象的时候就创建出来了,开辟了空间,占用了内存资源,懒汉什么时候用什么时候创建。
其实在现在计算机内存很大,可以不用考虑,但是做嵌入式开发内存较少这个时候可以用懒汉模式。
四、两者线程安全方面比较
饿汉模式没有线程安全问题,多线程的场景下,所谓的线程安全是多线程可以同时访问单例对象。
懒汉模式是有线程安全问题的、在线程进来的时候也就是getInstance来一个、new一个,为了解决这种问题我们的第一想法就是加互斥锁把线程阻塞多个线程依次访问单例对象,可以避免在懒汉模式下多个线程同时访问单例对象创建出多个类的实例的问题,和单例模式相对立了。以下是加锁的代码,不要忘记加头文件#include <mutex>
cpp
class TaskQueue
{
public:
TaskQueue(const TaskQueue &t) = delete;
TaskQueue& operator = (const TaskQueue &t) = delete;
static TaskQueue* getInstance()
{
m_mutex.lock();
if (m_taskQ == nullptr)
{
m_taskQ = new TaskQueue;
}
m_mutex.unlock();
return m_taskQ;
}
void print()
{
cout << "我是单例对象里面的成员函数!" << endl;
}
private:
TaskQueue() = default;
//只能通过类名访问静态属性方法
static TaskQueue* m_taskQ;
static mutex m_mutex;
};
//类外初始化
TaskQueue* TaskQueue::m_taskQ = nullptr;
mutex TaskQueue::m_mutex;
int main()
{
TaskQueue* taskQ = TaskQueue::getInstance();
taskQ->print();
return 0;
}
虽然我们通过锁机制可以解决这种问题但是来的线程一个一个进效率是不是就会很低所以我们可以使用一种方法:双重检查锁定,多加了一个判断。
cpp
class TaskQueue
{
public:
TaskQueue(const TaskQueue &t) = delete;
TaskQueue& operator = (const TaskQueue &t) = delete;
static TaskQueue* getInstance()
{
if (m_taskQ == nullptr)
{
m_mutex.lock();
if (m_taskQ == nullptr)
{
m_taskQ = new TaskQueue;
}
m_mutex.unlock();
}
return m_taskQ;
}
void print()
{
cout << "我是单例对象里面的成员函数!" << endl;
}
private:
TaskQueue() = default;
//只能通过类名访问静态属性方法
static TaskQueue* m_taskQ;
static mutex m_mutex;
};
//类外初始化
TaskQueue* TaskQueue::m_taskQ = nullptr;
mutex TaskQueue::m_mutex;
int main()
{
TaskQueue* taskQ = TaskQueue::getInstance();
taskQ->print();
return 0;
}
假如第一波有三个线程a b c同时进入在互斥锁依次进入,第一个 a 创建实例化之后m_tasQ已经不为空,b c直接解锁拿资源离开。
在这里可能效率没有改变和不同双重检查锁定一样,但是再加入判断检查之后随着后续再有线程进入那效率可就提高很多。
第二波再来三个线程d e f 刚进来到第一个判断,m_tasQ就不为空根本不进入互斥锁,直接拿着资源走人,也就是说我根本不需要进入以下代码段:
可以看到访问效率得到了提高。
解决线程安全方法还有好多,这里只提供一种方法,希望您可以对单例模式有一个初步理解!