C++设计模式创建型之单例模式

一、概述

单例模式也称单态模式,是一种创建型模式,用于创建只能产生一个对象实例的类。例如,项目中只存在一个声音管理系统、一个配置系统、一个文件管理系统、一个日志系统等,甚至如果吧整个Windows操作系统看成一个项目,那么其中只存在一个任务管理器窗口等。引入单例模式的实现意图:保证一个类仅有一个实例存在,同时提供能对该实例访问的全局方法。

二、单例模式分类

1、懒汉模式

1)代码示例

class CSingletonImpl

{

public:

static CSingletonImpl* GetInstance()

{

if (m_pInstance == nullptr)

{

m_pInstance = new CSingletonImpl;

}

return m_pInstance;

}

private:

CSingletonImpl(){};

~CSingletonImpl(){};

CSingletonImpl(const CSingletonImpl& the);

CSingletonImpl& operator=(const CSingletonImpl& other);

private:

static CSingletonImpl* m_pInstance;

};

CSingletonImpl*CSingletonImpl::m_pInstance = nullptr;

2)说明

单例模式为了防止多对象问题,将构造函数,析构函数,拷贝构造函数,赋值运算符函数设置为私有,同时设置公有唯一接口方法来创建对象,同时定义类静态指针。这是通用方法,那么会有什么问题呢?如果在单一线程中使用则没什么问题,但是在多线程中使用则可能导致问题,如果多个线程可能会因为操作系统时间片调度问题切换造成多对象产生,那么解决这个问题的方案就是对GetInstance()成员函数枷锁。

示例代码:

加入私有成员变量:static std::mutex m_mutex;

static CSingletonImpl* GetInstance()

{

m_mutex.lock();

if (m_pInstance == nullptr)

{

m_pInstance = new CSingletonImpl;

}

m_mutex.unlock();

return m_pInstance;

}

加入以上代码没有问题了吗?呵呵,还不行,虽然对接口函数加锁,从代码逻辑上没有问题,实现了线程安全,但是从执行效率上来说,是有大问题的。当程序运行中GetInstance()可能会被多个线程频繁调用,每次调用都会经历加锁解锁的过程,这样的话会严重影响程序执行效率,而且加锁机制仅仅对第一次创建对象有意义,对象一旦创建则变成只读对象,在多线程中,对只读对象的访问加锁不仅代价大,而且无意义。那么如何解决这个问题呢?那就是双重锁定机制,基于这种机制函数实现代码:

static CSingletonImpl* GetInstance()

{

if (m_pInstance == nullptr)

{

std::lock_guard<std::mutex> siguard(si_mutex);

if (m_pInstance == nullptr)

{

m_pInstance = new CSingletonImpl;

}

}

return m_pInstance;

}

上述双重锁定机制看起来比较完美,但实际上存在潜在的问题,内存访问重新排序导致双重锁定失效的问题,比较推荐的方法时C++11新标准的一些特性,示例代码如下:

#include <mutex>

#include <atomic>

//通过原子变量解决双重锁定底层问题(load,store)

class CSingletonImpl

{

public:

static CSingletonImpl* GetInstance()

{

CSingletonImpl* task = m_taskQ.load(std::memory_order_relaxed);

std::atomic_thread_fence(std::memory_order_acquire);

if (task == nullptr)

{

std::lock_guard<std::m_mutex> lock(m_mutex);

task = m_taskQ.load(std::memory_order_relaxed);

if (task == nullptr)

{

task = new CSingletonImpl;

std::atomic_thread_fence(std::memory_order_release);

m_taskQ.store(task, std::memory_order_relaxed);

}

}

return task;

}

private:

CSingletonImpl(){};

~CSingletonImpl(){};

CSingletonImpl(const CSingletonImpl& the);

CSingletonImpl& operator=(const CSingletonImpl& other);

private:

static std::mutex m_mutex;

static std::atomic<CSingletonImpl*> m_taskQ;

};

std::mutex CSingletonImpl::m_mutex;

std::atomic<CSingletonImpl*> CSingletonImpl::m_taskQ;

2、饿汉模式

1)示例代码

class CSingletonImpl

{

public:

static CSingletonImpl* GetInstance()

{

return m_pInstance;

}

private:

CSingletonImpl(){};

~CSingletonImpl(){};

CSingletonImpl(const CSingletonImpl& the);

CSingletonImpl& operator=(const CSingletonImpl& other);

private:

static CSingletonImpl* m_pInstance;

};

CSingletonImpl*CSingletonImpl::m_pInstance = new CSingletonImpl();

2)说明

此类模式可称为饿汉式--------程序一执行不管是否调用了GetInstance()成员函数,这个单例类对象就已经被创建了。在饿汉式单例类代码的实现必须要注意,如果一个项目中有多个.cpp源文件,而且这些源文件中包含对全局变量的初始化代码,例如某个.cpp中可能存在如下代码:

int g_test = CSingletonImpl::GetInstance()->m_i; //m_i是int类型变量

那么这样的代码是不安全的,因为多个源文件中全局变量的初始化顺序是不确定的,很可能造成GetInstance()函数返回是nullptr,此时去访问m_i成员变量肯定会导致程序执行异常。所以,对饿汉式单例类对象的使用,应该在程序入口函数开始执行后,例如main函数后。

注意:函数第一次执行时被初始化的静态变量与通过编译器常量进行初始化的基本类型静态变量这两种情况,不要再单例类的析构函数中引用其他单例类对象。

相关推荐
kloc10 分钟前
聊天室项目入口网关子服务的实现
linux·c++
csdn_aspnet3 小时前
C++ 3个有序点的方向(Orientation of 3 ordered points)
c++·算法
小七在进步4 小时前
string的使用(2)
c++
青少儿编程课堂5 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:折半搜索优化子集枚举(Meet in the Middle)解析
c++·python·算法·bfs·信息学竞赛
wo_jack7 小时前
C++ 入门笔记(十七):list 入门 —— 为什么 list 不能 sort?
数据结构·c++·面试·stl·list
进击的荆棘7 小时前
Linux系统——线程同步与互斥之线程池和安全
linux·运维·服务器·安全·单例模式·线程
青少儿编程课堂8 小时前
蓝桥杯青少组图形化 + 全国青少年信息素养大赛 + 电子学会Scratch考级:校园连连看解析
c++·python·算法·bfs·信息学竞赛
草莓熊Lotso8 小时前
【C++ 基于微服务的即时通讯系统】文件存储子服务实战:UUID生成、文件IO封装与全接口单元测试
开发语言·网络·数据库·c++·微服务·架构·单元测试
是2的10次方啊10 小时前
设计模式与生活⑲:投影连不上,别急着换投影仪
设计模式
David猪大卫1 天前
【C++修炼】map 和 set 的使用
c语言·开发语言·数据结构·c++·经验分享·笔记·学习