单例模式笔记总结

单例模式笔记总结

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

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

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

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

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

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

  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;  
};
相关推荐
郝学胜-神的一滴3 分钟前
Qt 高级开发 022:栅格布局深度实战
开发语言·c++·qt·软件构建·用户界面
basketball6164 分钟前
设计模式入门:3. 装饰器模式详解 C++实现
c++·设计模式·装饰器模式
土狗TuGou9 分钟前
SQL进阶笔记 · 第1篇:存储引擎
java·数据库·笔记·后端·sql·mysql
程序大视界16 分钟前
【C++ 从基础到项目实战】C++(三):函数进阶——重载、回调、递归与默认参数
开发语言·c++·cpp
西梅汁20 分钟前
C++ 线程间通信(二)
c++
minji...23 分钟前
Linux 高级IO(七)多进程、多线程的Reactor反应堆模式扩展、OTOL
linux·运维·c++·多路转接·epoll·reactor反应堆模型
晚风吹红霞24 分钟前
C++ list 容器完全指南:从入门到手撕双向链表
c++·链表·list
飞翔中文网25 分钟前
Java学习笔记之注解
java·笔记·学习
handler0125 分钟前
【Linux 网络】:poll/epoll 底层机制与 Reactor 并发模型
linux·运维·服务器·网络·c++·多路转接·多路复用
cpp_250126 分钟前
P10109 [GESP202312 六级] 工作沟通
数据结构·c++·算法·题解·洛谷·gesp六级