单例模式笔记总结

单例模式笔记总结

首先它都叫单例模式了,那它的特点就是全局只有唯一一个实例。所以这个设计模式的核心就是围绕着如何维护全局唯一实例。

如何保证全局只有一个实例

不妨先想想有什么办法可以实例化对象,再将路堵死

  • 首先想到的就是构造函数
    • 不能在类外使用构造函数构造一个实例
  • 其次就是拷贝构造函数
    • 拷贝构造函数就是用一个旧实例初始化一个新实例,所以也不能在类外使用
  • 最后就是拷贝赋值操作符
    • 拷贝赋值操作会将右值浅拷贝再赋值

以上就是它可以造出实例的办法了,那么只需要在类中设为私有或禁用 即可

**注意: 构造函数不能禁用,只能私有化。否则一个实例都构造不出来

  1. 禁用
c++ 复制代码
class TaskQueue{  
public:  
    TaskQueue(const TaskQueue& t) = delete;  
    TaskQueue& operator= (const TaskQueue& t) = delete;  
private:  
    TaskQueue() = default;  
};

2.私有化

c++ 复制代码
class TaskQueue{  
public:  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
};

现在在类外是无法实例化对象了,而我们又必须要一个实例化对象,只能在类内声明了。

  • 那现在考虑一个问题:
    • 用普通的成员变量可以吗?
    • 答案是否定的。因为如果是普通的成员变量,那怎么拿到这唯一的实例呢,通过成员函数吗?
      • 显然是不行的(类外无法构造出实例了,用不了成员函数)。
      • 那有什么函数是不用实例化对象就可以调用的呢?
      • 显然只有静态函数了,那普通成员变量在静态函数中能访问到吗?
      • 显然也是不行的,静态函数里只能访问静态成员变量。

综上我们总结出了:静态成员+静态函数

懒汉模式与饿汉模式

饿汉模式

饿汉模式就是在类加载时就初始化实例,通过静态函数返回实例。

  • 为什么叫饿汉模式呢?因为在类加载时就初始化了,但不知道什么时候会用它,所以它一直保持"饥饿"状态。(我是这么理解的)
c++ 复制代码
class TaskQueue{  
public:  
    static TaskQueue* getInstance(){  
        return m_tasks;  
    }  
  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
  
    static TaskQueue* m_tasks;  
};  
TaskQueue* TaskQueue::m_tasks = new TaskQueue();

懒汉模式

懒汉模式就是第一次通过静态函数返回实例时才初始化。

  • 为什么叫懒汉模式呢?因为它将初始化拖到了使用时才进行,很懒哈,临时抱佛脚。
c++ 复制代码
class TaskQueue{  
public:  
    static TaskQueue* getInstance(){  
        if(m_tasks==nullptr){  
            m_tasks = new TaskQueue();  
        }  
        return m_tasks;  
    }  
  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
  
    static TaskQueue* m_tasks;  
};  
TaskQueue* TaskQueue::m_tasks = nullptr;

懒汉模式下多线程的问题

**在多线程下懒汉模式有重复构造实例的隐患

  • 如果现在有两个线程同时获取实例,两个线程同时判断m_tasks==nullptr,都进入了构造逻辑,生成了两个实例
    那怎么办呢?加锁
c++ 复制代码
class TaskQueue{  
public:  
    static TaskQueue* getInstance(){  
        {  
            lock_guard<mutex> lg(m_mutex);  
            if (m_tasks == nullptr) {  
                m_tasks = new TaskQueue();  
            }  
        }  
        return m_tasks;  
    }  
  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
  
    static TaskQueue* m_tasks;  
    static mutex m_mutex;  
};  
TaskQueue* TaskQueue::m_tasks = nullptr;  
mutex TaskQueue::m_mutex;

如果是按照上面的情况加锁,每次访问实例都要等待资源,效率太慢。其实只有初始化时才需要上锁,所以我们可以判断:**如果已经初始化了那就可以不用等待资源直接返回实例。

c++ 复制代码
class TaskQueue{  
public:  
    static TaskQueue* getInstance() {  
        if(m_tasks==nullptr) {  
            {  
                lock_guard<mutex> lg(m_mutex);  
                if (m_tasks == nullptr) {  
                    m_tasks = new TaskQueue();  
                }  
            }  
        }  
        return m_tasks;  
    }  
  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
  
    static TaskQueue* m_tasks;  
    static mutex m_mutex;  
};  
TaskQueue* TaskQueue::m_tasks = nullptr;  
mutex TaskQueue::m_mutex;

**注意:内层还是需要if再判断一次的,如果现在有两个线程同时获取实例,两个线程同时判断m_tasks==nullptr,都进入了构造逻辑,就会生成了两个实例


问题来了,现在就安全了吗?

  • 看起来是天衣无缝,但是m_tasks = new TaskQueue();在底层正常会按顺序执行:
    • 分配内存
    • 初始化对象
    • 赋值操作
  • 但是编译器为了优化会进行**指令重排序
    • 分配内存
    • 赋值操作
    • 初始化对象
      现在有一个场景:两个线程访问实例,其中一个线程走到m_tasks = new TaskQueue();遇到了指令重排,先赋值没有初始化对象,那么另外一个线程发现指针不为nullptr,直接返回了一个未知对象。

解决方法:c++11中的atomic原子变量,它默认固定了指令的顺序(当然也可以通过load修改顺序)

  • 分配内存
  • 初始化对象
  • 赋值操作
c++ 复制代码
class TaskQueue{  
public:  
    static TaskQueue* getInstance() {  
  
        if(m_tasks==nullptr) {  
            {  
                lock_guard<mutex> lg(m_mutex);  
                if (m_tasks == nullptr) {  
                    m_tasks = new TaskQueue();  
                }  
            }  
        }  
        return m_tasks;  
    }  
  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
  
    static atomic<TaskQueue*> m_tasks;  
    static mutex m_mutex;  
};  
atomic<TaskQueue*> TaskQueue::m_tasks = nullptr;  
mutex TaskQueue::m_mutex;

局部静态对象实例

最轮椅的来了。没什么好解释的。

缺点:

  • 只能初始化一次
  • 只有等程序退出才会销毁对象内存
    优点:
  • 代码简洁
c++ 复制代码
class TaskQueue{  
public:  
    static TaskQueue* getInstance() {  
        static TaskQueue tasks;  
        return &tasks;  
    }  
  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
};
相关推荐
疯狂打码的少年9 分钟前
【软件工程】需求工程:需求获取与需求分析
数据库·笔记·软件工程·需求分析
小肝一下21 分钟前
2. 顺序表和链表
c语言·数据结构·c++·链表·dijkstra·依蕾娜·香香的依蕾娜
Lhappy嘻嘻38 分钟前
网络初识|基础入门:局域网广域网、IP 端口、TCP/IP 五层模型与封装解封装全过程
java·开发语言·网络·笔记·网络协议·tcp/ip
LiaoWL1231 小时前
【SpringCloud合集-06】Spring Cloud Gateway网关学习笔记
笔记·学习·spring cloud
王老师青少年编程1 小时前
csp信奥赛C++高频考点专项训练:【二分答案】案例5:进击的奶牛
c++·二分答案·csp·高频考点·信奥赛·进击的奶牛
鱼听禅1 小时前
ubuntu学习笔记-使用ls-ml-toolkit工具识别Label Studio数据数据训练yolo模型
笔记·学习·ubuntu
喜欢就别7 小时前
【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (2)--- On-Policy Distillation
人工智能·笔记
Lhappy嘻嘻9 小时前
Java IO|File 文件操作 + 字节流 / 字符流完整笔记 + 递归删除文件实战
java·笔记·php
GLDbalala11 小时前
GPU PRO 5 - 4.6 Adaptive Scalable Texture Compression 笔记
笔记
xian_wwq11 小时前
【学习笔记】模型权重管理:Safetensors与私有化Hub(23/35)
笔记·学习