news 2026/8/3 19:32:02

【设计模式】创建型模式01:单例模式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【设计模式】创建型模式01:单例模式

单例模式


OVERVIOW

  • 单例模式
      • 1.单例模式实现
      • 2.饿汉与懒汉
        • (1)饿汉模式
        • (2)懒汉模式
      • 3.懒汉线程安全-处理方式1
        • (1)引入互斥锁
        • (2)引入双重检查锁定
        • (3)引入原子变量
      • 4.懒汉线程安全-处理方式2
        • (1)设置局部静态对象
      • 5.案例运用
        • (1)任务队列简单实现
        • (2)用户登录

项目全局范围内,某个类的实例有且仅有一个,通过这个实例向其他模块提供数据的全局访问,这种模式就叫单例模式。

单例模式的典型应用就是任务队列。使用单例模式来替代全局变量(对全局变量进行管理),直接使用全局变量会破坏类的封装(全局变量随意读写),通过单例模式的类提供的成员函数进行访问。

单例模式优点:

  1. 提高性能:避免频繁的创建销毁对象,提高性能,
  2. 节省内存空间:在内存中只有一个对象,节省内存空间,
  3. 避免多重占用:避免对共享资源的多重占用,
  4. 全局访问:可全局访问,利用单例模式避免全局变量的出现

单例模式缺点:

  1. 扩展困难:单例模式中没有抽象层,因此扩展困难,

  2. 不适用于变化的对象:如果同类型的对象总是要在不同的用例场景发生变化,单例就会引起数据错误,不能保存状态。

  3. 职责过重:违背了单一职责原则

  4. 负面问题:

    为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多,而出现连接池溢出。

    如果实例化的单例对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

单利模式使用场景:

  1. 需要频繁实例化然后销毁的对象
  2. 创建对象耗时过多or消耗资源过多,但又经常使用到的对象,
  3. 有状态的工具类对象,
  4. 频繁访问数据库或文件的对象,
  5. 要求只有一个对象的场景

1.单例模式实现

如果使用单例模式,首先要保证这个类的实例有且仅有一个。因此就必须采取一些操作,涉及一个类多对象操作的函数有以下几个:

  • 构造函数:创建一个新的对象
  • 拷贝构造函数:根据已有对象拷贝出一个新的对象
  • 拷贝赋值操作符重载函数:两个对象之间的赋值

为了把一个类可以实例化多个对象的路堵死,需要对以上几个函数做如下处理:

  1. 构造函数私有化,在类内部只调用一次这是可控的。
    • 由于类外部不能使用构造函数,所以在类内部创建的唯一的对象必须是静态的,这样就可以通过类名来访问了,为了不破坏类的封装把这个静态对象设置为私有。
    • 在类中只有它的静态成员函数才能访问其静态成员变量,所以给这个单例类提供一个静态函数用于得到这个静态的单例对象。
  2. 拷贝构造函数私有化或者禁用(使用 = delete)
  3. 拷贝赋值操作符重载函数私有化或者禁用(从单例的语义上讲该函数已经毫无意义,所以在类中不再提供这样一个函数,故将它也一并处理)

单例模式就是给类创建一个唯一的实例对象,UML类图如下:

#include<iostream>usingnamespacestd;/* 1.关于类创建后的默认提供的函数 - 在创建一个新的类之后 会默认提供3个构造函数 1个析构函数 - 2个操作符重载(移动赋值操作符重载、拷贝赋值操作符重载)移动构造函数 拷贝构造函数 2.关于单例模式下类的实例化 - 在通过将 无参构造函数、拷贝构造函数、拷贝赋值操作符重载函数禁用之后 TaskQueue类已经无法在外部创建任何的对象 - 要得到TaskQueue的实例无法通过new操作符得到 只能通过类名得到(需要将对象设置为静态对象) - 通过类名访问类内部的属性和方法 其属性和方法一定是静态的(若不是静态需要通过对象来调用) - 能够操作静态成员变量的函数 只有静态成员函数 *///单例模式任务队列classTaskQueue{public://无参构造函数//TaskQueue() = delete;//拷贝构造函数TaskQueue(constTaskQueue&t)=delete;//赋值操作符重载函数TaskQueue&operator=(constTaskQueue&t)=delete;// = delete 代表函数禁用, 也可以将其访问权限设置为私有//静态成员公共函数用于获取实例staticTaskQueue*getInstance(){returnm_taskq;}voidprintTest(){cout<<"i am a public method of a singleton class"<<endl;}private://无参构造函数TaskQueue()=default;//拷贝构造函数//TaskQueue(const TaskQueue &t) = default;//赋值操作符重载函数//TaskQueue& operator=(const TaskQueue &t) = default;//通过类名访问静态属性或方法来创建类实例(需要在类外部做初始化处理)staticTaskQueue*m_taskq;};//静态成员初始化放到类外部处理TaskQueue*TaskQueue::m_taskq=newTaskQueue;intmain(){//获取TaskQueue的单例对象 由m_taskq指针指向TaskQueue*m_taskq=TaskQueue::getInstance();//由m_taskq指针调用单例类内部的成员方法m_taskq->printTest();return0;}

以上为单例模式中的饿汉模式,在定义单例类的时候就将类对应的单例对象一并创建出来了。

2.饿汉与懒汉

在实现一个单例模式的类的时候,有两种处理模式:

  • 饿汉模式:在将单例类定义出来后实例就已经存在了,饿汉模式没有线程安全问题
  • 懒汉模式:在使用单例对象的时候才会去创建单例对象的实例(节省内存空间),懒汉模式存在线程安全问题(多个线程同时访问单例的实例)
(1)饿汉模式
  1. 多个线程在访问单例对象时,没有线程安全问题,单例对象已经存在,不会出现多个线程创建出多个单例对象的情况。
  2. 多线程拿到单例对象后,在访问单例对象内部的数据时,有线程安全问题(多线程共享资源),
//饿汉模式classTaskQueue{public:TaskQueue(constTaskQueue&t)=delete;TaskQueue&operator=(constTaskQueue&t)=delete;staticTaskQueue*getInstance(){returnm_taskq;}voidprintTest(){cout<<"i am a public method of a singleton class"<<endl;}private:TaskQueue()=default;staticTaskQueue*m_taskq;};TaskQueue*TaskQueue::m_taskq=newTaskQueue;
//饿汉模式classTaskQueue{public:TaskQueue(constTaskQueue&t)=delete;TaskQueue&operator=(constTaskQueue&t)=delete;staticTaskQueue*getInstance(){return&m_taskq;}voidprintTest(){cout<<"i am a public method of a singleton class"<<endl;}private:TaskQueue()=default;staticTaskQueue m_taskq;//已经创建对象};TaskQueue*TaskQueue::m_taskq;//改为对象声明
(2)懒汉模式
//懒汉模式classTaskQueue{public:TaskQueue(constTaskQueue&t)=delete;TaskQueue&operator=(constTaskQueue&t)=delete;staticTaskQueue*getInstance(){if(m_taskq==nullptr)m_taskq=newTaskQueue;returnm_taskq;}voidprintTest(){cout<<"i am a public method of a singleton class"<<endl;}private:TaskQueue()=default;staticTaskQueue*m_taskq;};TaskQueue*TaskQueue::m_taskq=nullptr;

3.懒汉线程安全-处理方式1

在单例模式中饿汉模式下,针对在多线程中可能存在的线程安全问题(创建多个实例),进行问题修改:

(1)引入互斥锁

在多线程环境下,有可能的情况是:多个线程同时进入到getInstance()方法中的if语句判断中,这时对象就可能被同时创建多个,

//懒汉模式 引入互斥锁classTaskQueue{public:TaskQueue(constTaskQueue&t)=delete;TaskQueue&operator=(constTaskQueue&t)=delete;staticTaskQueue*getInstance(){m_mutex.lock();if(m_taskq==nullptr)m_taskq=newTaskQueue;m_mutex.unlock();returnm_taskq;}voidprintTest(){cout<<"i am a public method of a singleton class"<<endl;}private:TaskQueue()=default;staticTaskQueue*m_taskq;staticmutex m_mutex;};mutex TaskQueue::m_mutex;TaskQueue*TaskQueue::m_taskq=nullptr;

使用互斥锁对new操作创建实例时进行加锁操作,防止同时创建多个实例,但是程序执行的效率太低(多线程访问单例对象时都是顺序访问)

(2)引入双重检查锁定

双重检查锁定,只有第一次访问时是顺序执行的,在TaskQueue被实例化出来之后,其他线程再去访问单例对象就是并行的了(不会进入if内)。

//懒汉模式 引入双重检查锁定classTaskQueue{public:TaskQueue(constTaskQueue&t)=delete;TaskQueue&operator=(constTaskQueue&t)=delete;staticTaskQueue*getInstance(){//双重检查锁定if(m_taskq==nullptr){m_mutex.lock();if(m_taskq==nullptr)m_taskq=newTaskQueue;m_mutex.unlock();}returnm_taskq;}voidprintTest(){cout<<"i am a public method of a singleton class"<<endl;}private:TaskQueue()=default;staticTaskQueue*m_taskq;staticmutex m_mutex;};mutex TaskQueue::m_mutex;TaskQueue*TaskQueue::m_taskq=nullptr;
(3)引入原子变量

通过引入双重检查锁定的方式,解决了在懒汉模式下多线程访问单例对象时,出现的线程安全问题,

从表面上观察引入双重检查锁定的方式是十分完美的,但是从底层上依旧存在漏洞:

  1. 对于m_taskq = new TaskQueue;操作,其对应的机器指令并不是一条,而有三条(对于计算机来说代码都是二进制指令/机器指令),

    step1:创建一块内存(没有数据) step2:创建 TaskQueue 类型的对象,并将数据写入到对象中 step3:为 m_taskq 对象指针初始化,将有效的内存地址传递给 m_taskq 对象指针
  2. 在实际的执行过程中,m_taskq = new TaskQueue;对应的机器指令可能会被重新排序,成为

    step1:创建一块内存(没有数据) step3:为 m_taskq 对象指针初始化,将有效的内存地址传递给 m_taskq 对象指针 step2:创建 TaskQueue 类型的对象,并将数据写入到对象中
  3. 如果线程A执行完成前两步之后失去CPU时间片被挂起,此时线程B在进行指针判断时,发现指针m_taskq不为空(但该指针指向内存没有被初始化),导致线程B使用了一个没有被初始化的队列对象,就会出现问题(出现问题是概率性的)

  4. 在C++11中引入原子变量 atomic,在底层控制机器指令的执行顺序,可以实现一种更加安全的懒汉模式,代码如下:

    使用原子变量atomicstore()方法来存储单例对象,使用load()方法来加载单例对象,

    在原子变量中这两个函数在处理指令的时候,默认的原子顺序是 memory_order_seq_cst 顺序原子操作,

    使用顺序约束原子操作库,整个函数的执行都将保证顺序执行,并且不会出现数据竞态 data races,

    缺点:使用这种方法实现的懒汉模式的单例执行效率更低一些,

    对代码进行以下修改:

    • 通过原子变量将类的实例对象保存起来(m_taskq 指针指向的内存)

    • 类外初始化 指针指向为nullptr

    • 对 getInstance 方法进行相关的修改操作

      多线层在调用 getInstance 方法时 需要从原子变量中加载任务队列的实例

      抢到互斥锁的线程将继续向下执行 创建实例对象

//懒汉模式 引入原子变量classTaskQueue{public:TaskQueue(constTaskQueue&t)=delete;TaskQueue&operator=(constTaskQueue&t)=delete;staticTaskQueue*getInstance(){TaskQueue*taskq=m_taskq.load();if(taskq==nullptr){m_mutex.lock();taskq=m_taskq.load();if(taskq==nullptr){taskq=newTaskQueue;m_taskq.store(taskq);}m_mutex.unlock();}returnm_taskq.load();}voidprintTest(){cout<<"i am a public method of a singleton class"<<endl;}private:TaskQueue()=default;// static TaskQueue *m_taskq;staticatomic<TaskQueue*>m_taskq;staticmutex m_mutex;};mutex TaskQueue::m_mutex;atomic<TaskQueue*>TaskQueue::m_taskq;// TaskQueue* TaskQueue::m_taskq = nullptr;

4.懒汉线程安全-处理方式2

在懒汉模式线程安全问题中,除了可以通过引入双重检查锁定来解决线程安全问题,还可以使用局部静态对象处理线程安全问题,

(1)设置局部静态对象

使用静态的局部对象解决线程安全问题,要求编译器必修支持C++11标准,

  1. getInstance()局部函数中定义一个静态局部对象static TaskQueue taskq;(调用无参构造初始化)
  2. 在C++11标准中规定,如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化,

注:使用静态的局部对象没有线程安全问题,已经由C++11标准中的编译器解决,未被初始化的变量,必须等待其完成初始化才能并发执行,

step1:创建一块内存(没有数据) step2:创建 TaskQueue 类型的对象,并将数据写入到对象中(完成初始化操作) step3:为 m_taskq 对象指针初始化,将有效的内存地址传递给 m_taskq 对象指针
// 懒汉模式 静态局部对象classTaskQueue{public:TaskQueue(constTaskQueue&t)=delete;TaskQueue&operator=(constTaskQueue&t)=delete;staticTaskQueue*getInstance(){staticTaskQueue taskq;return&taskq;}voidprintTest(){cout<<"i am a public method of a singleton class"<<endl;}private:TaskQueue()=default;};

5.案例运用

(1)任务队列简单实现
  1. 多线程拿到单例对象后,在访问单例对象内部的数据时,有线程安全问题(多线程共享资源),使用互斥锁保护多线程中共享的资源,

  2. C++11中给互斥锁加/解锁有两种方式,

    方法1:调用mutex对象的unlock();lock();方法

    方法2:使用lock_gurd自动管理加/解锁lock_guard<mutex> locker(m_mutex);

    使用lock_gurd可以有效的避免死锁的问题,自动加/解锁

// 饿汉模式classTaskQueue{public:TaskQueue(constTaskQueue&t)=delete;TaskQueue&operator=(constTaskQueue&t)=delete;staticTaskQueue*getInstance(){returnm_taskq;}voidprintTest(){cout<<"i am a public method of a singleton class"<<endl;}// 判断任务队列是否为空boolisEmpty(){lock_guard<mutex>locker(m_mutex);returnm_data.empty();}// 添加任务voidaddTask(intnode){lock_guard<mutex>locker(m_mutex);m_data.push(node);}// 删除任务boolremoveTask(){lock_guard<mutex>locker(m_mutex);if(m_data.empty())returnfalse;m_data.pop();returntrue;}// 获取队头任务inttakeTask(){lock_guard<mutex>locker(m_mutex);if(m_data.empty())return-1;returnm_data.front();}private:TaskQueue()=default;staticTaskQueue*m_taskq;// 任务队列queue<int>m_data;mutex m_mutex;};TaskQueue*TaskQueue::m_taskq=newTaskQueue;
#include<iostream>#include<thread>#include<queue>#include<mutex>usingnamespacestd;intmain(){// 获取单例对象TaskQueue*taskq=TaskQueue::getInstance();taskq->printTest();// 生产者线程// 使用匿名函数指定线程的处理动作threadt1([=](){for(inti=0;i<25;++i){taskq->addTask(i+100);cout<<"++push data:"<<i+100<<", threadId = "<<this_thread::get_id()<<endl;this_thread::sleep_for(chrono::milliseconds(500));//休眠500ms}});// 消费者线程threadt2([=](){this_thread::sleep_for(chrono::milliseconds(100));while(!taskq->isEmpty()){// 开始消费cout<<"--take data:"<<taskq->takeTask()<<", threadId = "<<this_thread::get_id()<<endl;taskq->removeTask();this_thread::sleep_for(chrono::milliseconds(1000));//休眠500ms}});// 主线程阻塞 只有当t1、t2线程都结束后 主线程解除阻塞t1.join();t2.join();return0;}
(2)用户登录

当用户成功登录之后,用户名和密码就会被存储到内存中,可以创建一个单例类,将用户数据保存到单例对象中,

classTest{public:staticTest*getInstance(){return&m_test;}// m_uservoidsetUserName(QString name){// 多线程下需要加锁解锁(涉及写操作)// lock();m_user=name;// unlock();}QStringgetUserName(){returnm_user;}// m_passwd// ....// ....// ....private:Test();Test(constTest&t);staticTest*m_test;// static Test m_test;// 定义变量 -> 属于唯一的单例对象QString m_user;QString m_passwd;QString m_ip;QString m_port;QString m_token;}Test*Test::m_test=newTest();// 初始化// Test Test::m_test;
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/3 19:30:44

Appium与Chromedriver组合:移动端WebView自动化测试完整指南

1. 项目概述&#xff1a;为什么我们需要 Appium 与 Chromedriver 的组合&#xff1f; 如果你正在做移动端自动化测试&#xff0c;尤其是涉及到 App 内的 WebView 或混合应用&#xff08;Hybrid App&#xff09;&#xff0c;那你大概率绕不开 Appium 和 Chromedriver 这对组合。…

作者头像 李华
网站建设 2026/8/3 19:30:41

芯片OS测试:从稳定性到电源管理的量产关键验证

1. 从“点亮”到“稳定”&#xff1a;为什么OS测试是芯片量产前的关键一役当一颗崭新的芯片从晶圆厂下线&#xff0c;经过封装&#xff0c;最终被送到测试工程师手中时&#xff0c;很多人会认为&#xff0c;最激动人心的时刻是第一次上电&#xff0c;看到指示灯亮起&#xff0c…

作者头像 李华
网站建设 2026/8/3 19:29:01

Robot Framework自动化测试入门:从核心概念到Web与接口实战

1. 项目概述&#xff1a;为什么Robot Framework值得你投入时间&#xff1f; 如果你正在寻找一个能快速上手、功能强大且社区活跃的自动化测试框架&#xff0c;Robot Framework&#xff08;后文简称RF&#xff09;绝对是一个绕不开的名字。我最早接触它是在一个大型的Web和接口混…

作者头像 李华
网站建设 2026/8/3 19:28:25

ApacheBench (ab) 压力测试工具:从入门到实战性能调优

1. 项目概述&#xff1a;为什么我们需要ab命令&#xff1f; 在网站开发和运维的日常工作中&#xff0c;性能始终是悬在头顶的达摩克利斯之剑。一个新功能上线&#xff0c;一个促销活动开启&#xff0c;最怕的就是服务器在流量洪峰面前“躺平”。作为一线工程师&#xff0c;我们…

作者头像 李华
网站建设 2026/8/3 19:27:40

智能网联汽车信任根与密钥管理技术解析

1. 智能网联汽车的安全基石&#xff1a;信任根的本质与挑战当一辆智能网联汽车以60公里时速行驶时&#xff0c;其车载系统每秒需要处理超过1000条来自各类传感器的数据流&#xff0c;并与云端服务器进行数十次加密通信。这个过程中&#xff0c;任何一处密钥泄露或身份伪造都可能…

作者头像 李华