您现在的位置是:首页 >技术教程 >设计模式-单例模式网站首页技术教程

设计模式-单例模式

小瑞的学习笔记 2024-06-10 00:00:02
简介设计模式-单例模式

说说什么是单例设计模式,如何实现

单例模式定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

那么我们就必须保证:

(1)该类不能被复制。

(2)该类不能被公开的创造。

那么对于C++来说,它的构造函数,拷贝构造函数和赋值函数都不能被公开调用。

单例模式实现方式

单例模式通常有两种模式,分别为懒汉式单例和饿汉式单例。

使用场景

  • 创建线程池
  • 连接数据库
  • 加载配置文件

饿汉模式

  • 饿汉模式就是在类加载的时候立刻进行实例化,这样就得到了一个唯一的可用对象。关于这个饿汉模式的类的定义如下:
// 饿汉模式
class TaskQueue
{
public:
    static TaskQueue* getInstance()
    {
        return m_taskQ;
    }
private:
    TaskQueue(const TaskQueue& obj);
    TaskQueue& operator=(const TaskQueue& obj);
    TaskQueue();
    
    static TaskQueue* m_taskQ;
};
// 静态成员初始化放到类外部处理
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;

int main()
{
    TaskQueue* obj = TaskQueue::getInstance();
}
  • 在第17行,定义这个单例类的时候,就把这个静态的单例对象创建出来了。当使用者通过 getInstance() 获取这个单例对象的时候,它已经被准备好了。
  • 注意事项:类的静态成员变量在使用之前必须在类的外部进行初始化才能使用

懒汉模式

单线程下使用

// 懒汉模式
class TaskQueue
{
public:
    static TaskQueue* getInstance()
    {
        if(m_taskQ == nullptr)
        {
            m_taskQ = new TaskQueue;
        }
        return m_taskQ;
    }
private:
    TaskQueue(const TaskQueue& obj);
    TaskQueue& operator=(const TaskQueue& obj);
    TaskQueue();
    
    static TaskQueue* m_taskQ;
};
TaskQueue* TaskQueue::m_taskQ = nullptr;
  • 在调用 getInstance() 函数获取单例对象的时候,如果在单线程情况下是没有什么问题的,如果是多个线程,调用这个函数去访问单例对象就有问题了。
  • 假设有三个线程同时执行了getInstance() 函数,在这个函数内部每个线程都会 new 出一个实例对象。
    此时,这个任务队列类的实例对象不是一个而是 3 个,很显然这与单例模式的定义是相悖的。

多线程下使用

  • 对于饿汉模式是没有线程安全问题的,在这种模式下访问单例对象的时候,这个对象已经被创建出来了。
  • 要解决懒汉模式的线程安全问题,最常用的解决方案就是使用互斥锁。可以将创建单例对象的代码使用互斥锁锁住,处理代码如下:
class TaskQueue
{
public:
    static TaskQueue* getInstance()
    {
        m_mutex.lock();// 加锁
        if (m_taskQ == nullptr)
        {
            m_taskQ = new TaskQueue;
        }
        m_mutex.unlock();// 解锁
        return m_taskQ;
    }
private:
    TaskQueue();
    TaskQueue(const TaskQueue& obj);
    TaskQueue& operator=(const TaskQueue& obj);


    static TaskQueue* m_taskQ;
    static mutex m_mutex;// 线程锁
};
TaskQueue* TaskQueue::m_taskQ = nullptr;
mutex TaskQueue::m_mutex;
  • 在上面代码的 10~13 行这个代码块被互斥锁锁住了
  • 也就意味着不论有多少个线程,同时执行这个代码块的线程只能是一个(相当于是严重限行了,在重负载情况下,可能导致响应缓慢)。我们可以将代码再优化一下:
class TaskQueue
{
public:
    // = delete 代表函数禁用, 也可以将其访问权限设置为私有
    TaskQueue(const TaskQueue& obj) = delete;
    TaskQueue& operator=(const TaskQueue& obj) = 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;
    }
private:
    TaskQueue() = default;
    static TaskQueue* m_taskQ;
    static mutex m_mutex;
};
TaskQueue* TaskQueue::m_taskQ = nullptr;
mutex TaskQueue::m_mutex;
  • 这样当任务队列的实例被创建出来之后,访问这个对象的线程就不会再执行加锁和解锁操作了
  • 上面这种通过两个嵌套的 if 来判断单例对象是否为空的操作就叫做双重检查锁定。

双重检查锁定的问题

  • 但是实际上 m_taskQ = new TaskQueue; 在执行过程中对应的机器指令可能会被重新排序。
  • 这样重排序并不影响单线程的执行结果,但是在多线程中就会出问题。

解决办法:

  • 在 C++11 中引入了原子变量 atomic,通过原子变量可以实现一种更安全的懒汉模式的单例,代码如下:
class TaskQueue
{
public:
    static TaskQueue* getInstance()
    {
        TaskQueue* queue = m_taskQ.load();// 把对象指针拿出来  
        if (queue == nullptr)
        {
            m_mutex.lock();  // 加锁
            queue = m_taskQ.load();// 把对象指针拿出来
            if (queue == nullptr)
            {
                queue = new TaskQueue;
                m_taskQ.store(queue);// 把对象指针输入进入
            }
            m_mutex.unlock();
        }
        return queue;
    }

    void print()
    {
        cout << "hello, world!!!" << endl;
    }
private:
    TaskQueue();
    TaskQueue(const TaskQueue& obj);
    TaskQueue& operator=(const TaskQueue& obj);

    static atomic<TaskQueue*> m_taskQ;
    static mutex m_mutex;
};
atomic<TaskQueue*> TaskQueue::m_taskQ = NULL;
mutex TaskQueue::m_mutex;

int main()
{
    TaskQueue* queue = TaskQueue::getInstance();
    queue->print();
    return 0;
}
  • 上面代码中使用原子变量 atomic 的 store() 方法来存储单例对象,使用 load() 方法来加载单例对象。

静态局部对象

  • 在实现懒汉模式的单例的时候,相较于双重检查锁定模式有一种更简单的实现方法并且不会出现线程安全问题,那就是使用静态局部局部对象,对应的代码实现如下:
class TaskQueue
{
public:
    static TaskQueue* getInstance()
    {
        static TaskQueue taskQ;
        return &taskQ;
    }
    void print()
    {
        cout << "hello, world!!!" << endl;
    }

private:
    TaskQueue(const TaskQueue& obj);
    TaskQueue& operator=(const TaskQueue& obj);
    TaskQueue() = default;
};

int main()
{
    TaskQueue* queue = TaskQueue::getInstance();
    queue->print();
    return 0;
}
  • 在程序的第 9、10 行定义了一个静态局部队列对象,并且将这个对象作为了唯一的单例实例。
  • 使用这种方式之所以是线程安全的,是因为在 C++11 标准中有如下规定:

如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。