• 【11】c++设计模式——>单例模式


    单例模式是什么

    在一个项目中,全局范围内,某个类的实例有且仅有一个(只能new一次),通过这个唯一的实例向其他模块提供数据的全局访问,这种模式就叫单例模式。单例模式的典型应用就是任务队列。

    为什么要使用单例模式

    单例模式充当的就是一个全局变量,为什么不直接使用全局变量呢,因为全局变量破坏类的封装,而且不受保护,访问不受限制。

    在这里插入图片描述

    饿汉模式

    饿汉模式是在类加载时进行实例化的。

    #include
    using namespace std;
    
    class TaskQueue
    {
    public:
    	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
    	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
    	static TaskQueue* getInstance()  //获取单例的方法
    	{
    		cout << "我是一个饿汉模式单例" << endl;
    		return m_taskQ;
    	}
    
    private:
    	TaskQueue() = default; //无参构造
    	static TaskQueue* m_taskQ; //静态成员需要在类外定义
    };
    TaskQueue* TaskQueue::m_taskQ = new TaskQueue;//new一个实例;
    
    int main()
    {
    	TaskQueue* obj = TaskQueue::getInstance();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    懒汉模式

    懒汉模式是在需要使用的时候再进行实例化

    #include
    using namespace std;
    
    class TaskQueue
    {
    public:
    	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
    	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
    	static TaskQueue* getInstance()  //获取单例的方法
    	{
    		if (nullptr == m_taskQ)
    		{
    			m_taskQ = new TaskQueue;
    		}
    		return m_taskQ;
    	}
    
    private:
    	TaskQueue() = default; //无参构造
    	static TaskQueue* m_taskQ; //静态成员需要在类外定义
    };
    TaskQueue* TaskQueue::m_taskQ = nullptr;
    
    int main()
    {
    	TaskQueue* obj = TaskQueue::getInstance();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在调用**getInstance()**函数获取单例对象的时候,如果在单线程情况下是没有什么问题的,如果是多个线程,调用这个函数去访问单例对象就有问题了。假设有三个线程同时执行了getInstance()函数,在这个函数内部每个线程都会new出一个实例对象。此时,这个任务队列类的实例对象不是一个而是3个,很显然这与单例模式的定义是相悖的。

    线程安全问题

    对于饿汉模式来说是没有线程安全问题的,在这种模式下访问单例对象时,这个对象已经被创建出来了,要解决懒汉模式的线程安全问题,最常用的解决方案就是使用互斥锁,可以将创建单例对象的代码使用互斥锁锁住:

    #include
    #include
    using namespace std;
    
    class TaskQueue
    {
    public:
    	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
    	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
    	static TaskQueue* getInstance()  //获取单例的方法
    	{
    		m_mutex.lock();
    		if (nullptr == m_taskQ)
    		{
    			cout << "我加了互斥锁" << endl;
    			m_taskQ = new TaskQueue;
    		}
    		m_mutex.unlock();
    		return m_taskQ;
    	}
    
    private:
    	TaskQueue() = default; //无参构造
    	static TaskQueue* m_taskQ; //静态成员需要在类外定义
    	static mutex m_mutex; //定义为静态的,因为静态函数只能使用静态变量
    };
    mutex TaskQueue::m_mutex;
    TaskQueue* TaskQueue::m_taskQ = nullptr;
    
    int main()
    {
    	TaskQueue* obj = TaskQueue::getInstance();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    在上面代码的10~13 行这个代码块被互斥锁锁住了,也就意味着不论有多少个线程,同时执行这个代码块的线程只能是一个(相当于是严重限行了,在重负载情况下,可能导致响应缓慢)。我们可以将代码再优化一下:

    #include
    #include
    using namespace std;
    
    class TaskQueue
    {
    public:
    	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
    	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
    	static TaskQueue* getInstance()  //获取单例的方法
    	{
    		if (nullptr == m_taskQ)
    		{
    			m_mutex.lock();
    			if (nullptr == m_taskQ)
    			{
    				cout << "我加了互斥锁" << endl;
    				m_taskQ = new TaskQueue;
    			}
    			m_mutex.unlock();
    		}
    		return m_taskQ;
    	}
    
    private:
    	TaskQueue() = default; //无参构造
    	static TaskQueue* m_taskQ; //静态成员需要在类外定义
    	static mutex m_mutex;
    };
    mutex TaskQueue::m_mutex;
    TaskQueue* TaskQueue::m_taskQ = nullptr;
    
    int main()
    {
    	TaskQueue* obj = TaskQueue::getInstance();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    双重检查锁定问题

    #include
    #include
    #include
    using namespace std;
    
    class TaskQueue
    {
    public:
    	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
    	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
    	static TaskQueue* getInstance()  //获取单例的方法
    	{
    		TaskQueue* taskQ = m_taskQ.load(); //取出来单例的值
    		if (nullptr == taskQ)
    		{
    			m_mutex.lock();
    			taskQ = m_taskQ.load();
    			if (nullptr == taskQ)
    			{
    				cout << "我加了原子" << endl;
    				taskQ = new TaskQueue;
    				m_taskQ.store(taskQ); //保存到原子变量中
    			}
    			m_mutex.unlock();
    		}
    		return m_taskQ.load();
    	}
    
    private:
    	TaskQueue() = default; //无参构造
    	static atomic<TaskQueue*>m_taskQ; //定义为原子变量
    	static mutex m_mutex;
    };
    mutex TaskQueue::m_mutex;
    atomic<TaskQueue*> TaskQueue::m_taskQ;
    
    
    int main()
    {
    	TaskQueue* obj = TaskQueue::getInstance();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    对于m_taskQ = new TaskQueue这行代码来说,我们期望的执行机器指令执行顺序是:
    (1)分配用来保存TaskQueue对象的内存;
    (2)在分配好的内存中构造一个TaskQueue对象(即初始化内存);
    (3)使用m_taskQ指针指向分配的内存。
    但是对多线程来说,机器指令可能会被重新排列;即:
    (1)分配内存用于保存 TaskQueue 对象。
    (2)使用 m_taskQ 指针指向分配的内存。
    (3)在分配的内存中构造一个 TaskQueue 对象(初始化内存)。
    这样重排序并不影响单线程的执行结果,但是在多线程中就会出问题。如果线程A按照第二种顺序执行机器指令,执行完前两步之后失去CPU时间片被挂起了,此时线程B在第3行处进行指针判断的时候m_taskQ 指针是不为空的,但这个指针指向的内存却没有被初始化,最后线程 B 使用了一个没有被初始化的队列对象就出问题了(出现这种情况是概率问题,需要反复的大量测试问题才可能会出现)。
    在C++11中引入了原子变量atomic,通过原子变量可以实现一种更安全的懒汉模式的单例,代码如下:

    #include
    #include
    #include
    using namespace std;
    
    class TaskQueue
    {
    public:
    	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
    	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
    	static TaskQueue* getInstance()  //获取单例的方法
    	{
    		TaskQueue* taskQ = m_taskQ.load(); //取出来单例的值
    		if (nullptr == taskQ)
    		{
    			m_mutex.lock();
    			taskQ = m_taskQ.load();
    			if (nullptr == taskQ)
    			{
    				cout << "我加了原子" << endl;
    				taskQ = new TaskQueue;
    				m_taskQ.store(taskQ); //保存到原子变量中
    			}
    			m_mutex.unlock();
    		}
    		return m_taskQ.load();
    	}
    
    private:
    	TaskQueue() = default; //无参构造
    	static atomic<TaskQueue*>m_taskQ; //定义为原子变量
    	static mutex m_mutex;
    };
    mutex TaskQueue::m_mutex;
    atomic<TaskQueue*> TaskQueue::m_taskQ;
    
    
    int main()
    {
    	TaskQueue* obj = TaskQueue::getInstance();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    使用局部静态

    c++11新特性有如下规定:如果指令逻辑进入一个未被初始化的声明标量,所有并发执行应当等待该变量完成初始化。

    #include
    
    using namespace std;
    
    class TaskQueue
    {
    public:
    	TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
    	TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
    	static TaskQueue* getInstance()  //获取单例的方法
    	{
    		static TaskQueue m_taskQ; //未被初始化
    		return &m_taskQ;
    	}
    	void print()
    	{
    		cout << "hello, world!!!" << endl;
    	}
    
    private:
    	TaskQueue() = default; //无参构造
    };
    
    
    
    int main()
    {
    	TaskQueue* obj = TaskQueue::getInstance();
    	obj->print();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    在C++中,当声明一个静态成员变量时,如果没有显式地初始化它,它将被默认初始化为零值或空值,具体取决于变量的类型。
    对于类对象类型的静态成员变量,会自动调用默认构造函数进行初始化。在这种情况下,m_taskQ 将会被初始化为TaskQueue类的默认构造函数创建的对象

    饿汉模式和懒汉模式的区别

    懒汉模式的缺点是在创建实例对象的时候有安全问题,但这样可以减少内存的浪费(如果用不到就不去申请内存了)。饿汉模式则相反,在我们不需要这个实例对象的时候,它已经被创建出来,占用了一块内存。对于现在的计算机而言,内存容量都是足够大的,这个缺陷可以被无视。

  • 相关阅读:
    基于多目标粒子群优化算法的冷热电联供型综合能源系统运行优化附Matlab代码
    黑马瑞吉外卖之员工账号的禁用和启用以及编辑修改
    SpringBoot自动装配
    Linux(Centos7版本中安装mysql5.7 遇到的各种问题,最后由于Centos7安装mysql5.7需要收费,安装了 mariadb 数据库)
    【iOS】—— pthread、NSThread
    信息学奥赛一本通:1151:素数个数
    什么是AI推理
    (算法设计与分析)第三章动态规划-第一节3:动态规划之使用“找零钱”问题说明最优子结构如何解决
    【LeetCode动态规划#13】买卖股票含冷冻期(状态众多,比较繁琐)、含手续费
    PDF格式分析(七十)——注释边框样式及外观流
  • 原文地址:https://blog.csdn.net/weixin_42097108/article/details/133561844