单例模式——C++版本

这篇具有很好参考价值的文章主要介绍了单例模式——C++版本。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1.什么是单例模式

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

类中多对象的操作函数有如下几个:

  • 构造函数 : 能够创建出一个新对象;
  • 拷贝构造函数 :能够根据一个已经存在的对象拷贝出一个新对象;
  • 赋值操作符重载函数 :用一个对象给另一个对象赋值;

为了使得类全局只有一个实例,我们需要对这些函数做一些处理

  • 构造函数私有化,且在类内部只被调用一次;
    • 由于使用者在类外部不能使用构造函数,所以在类内部创建的这个唯一的对象必须是静态的,这样就可以通过类名来访问了,为了不破坏类的封装,我们都会把这个静态对象的访问权限设置为 private
    • 类中只有它的静态成员函数才能访问其静态成员变量,所以可以给这个单例类提供一个静态函数用于得到这个静态的实例对象
  • 拷贝构造函数 私有化 或者 禁用(private 或者 delete);
  • 赋值操作符重载函数私有化 或者 禁用。(这个操作有没有都没影响)

单例模式的代码模板:

// 定义一个单例模式的类
class Singleton
{
public:
    // = delete 代表函数禁用, 也可以将其访问权限设置为私有
    Singleton(const Singleton& rhs) = delete;
    Singleton& operator=(const Singleton& rhs) = delete;
    static Singleton* getInstance();
private:
    Singleton() = default;
    static Singleton* m_obj;
};

2.单例模式

单例模式可以分为 :懒汉式饿汉式

一、懒汉式

饿汉模式就是在类加载的时候立刻进行实例化,这样就得到了一个唯一的可用对象。

定义:

// 饿汉模式 在调用 get_instance 之前 实例就已经存在了
// 多线程环境下 , 饿汉模式是线程 安全的
class TaskQueue {
public:
	TaskQueue(const TaskQueue& rhs) = delete;
	TaskQueue& operator = (const TaskQueue& rhs) = delete;

	static TaskQueue* get_instance() {
		return m_task_queue;
	}

	void print() {
		cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;
	}

private:
	TaskQueue() = default;
	static TaskQueue* m_task_queue;
};

TaskQueue* TaskQueue::m_task_queue = new TaskQueue;

int main()
{
    TaskQueue* task_queue = TaskQueue::getInstance();
    task_queue->print();
}

需要注意的是:

  • 在定义这个 TaskQueue 类的时候,这个静态的单例对象 m_task_queue 就已经被创建出来了,当调用 TaskQueue::get_instance() 的时候,对象就已经被实例化了;
  • 类中的静态成员变量需要在类外初始化
  • 饿汉式在多线程环境下是线程安全的

二、懒汉式

懒汉式是在类加载的时候不去创建这个唯一的实例,而是在需要使用的时候再进行实例化

定义:

// 懒汉模式 在调用 get_instance 之前 实例存在 , 第一次调用 get_instance 才会实例化对象
// 多线程环境下, 饿汉模式是线程 不安全的
class TaskQueue {
public:
	TaskQueue(const TaskQueue& rhs) = delete;
	TaskQueue& operator = (const TaskQueue& rhs) = delete;

	static TaskQueue* get_instance() {
		if (m_task_queue == nullptr) {
		   //在第一次调用 get_instance() 的时候再初始化
			m_task_queue = new TaskQueue;
		}
		return m_task_queue;
	}

	void print() {
		cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;
	}

private:
	TaskQueue() = default;
	static TaskQueue* m_task_queue;
};

TaskQueue* TaskQueue::m_task_queue = nullptr;


int main()
{
    TaskQueue* task_queue = TaskQueue::getInstance();
    task_queue->print();
}

上述代码在单线程环境下是没问题的。但是在多线程环境下,就会出问题,假设多个线程同时调用 get_instance() 函数,并且此时 m_task_queue = nullptr,那么就可能创建出多个实例,这就不符合单例模式的定义。

解决方案一:加锁(效率比较低)

我们可以使用互斥锁 mutex 将创建实例的代码锁住,第一次只有一个线程进来创建对象。

代码:

// 用 双重检测锁定 解决懒汉式多线程环境下线程不安全的问题
class TaskQueue {
public:
	TaskQueue(const TaskQueue& rhs) = delete;
	TaskQueue& operator = (const TaskQueue& rhs) = delete;

	static TaskQueue* get_instance() {
		m_mutex.lock(); //加锁
		
        if (m_task_queue== nullptr)
        {
            m_task_queue= new TaskQueue;
        }
        
        m_mutex.unlock(); //解锁
        return m_task_queue;
	}

	void print() {
		cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;
	}

private:
	TaskQueue() = default;
	static TaskQueue* m_task_queue;
	static mutex m_mutex;
};

TaskQueue* TaskQueue::m_task_queue = nullptr;
mutex TaskQueue::m_mutex;

上面代码虽然解决了问题,但是 get_instance() 中的锁住的代码段,每次就只有一个线程来访问,这样效率就非常低

解决方法二:双重检测锁定(存在问题)

双重检测锁定的思路是:在加锁和解锁代码块 之外再加一个 if 判断。这样的话,在第一次调用 get_instance() 的线程仍然会阻塞;第二次调用 get_instance() 的线程,此时 m_task_queue 已经被实例化了,也就是不为 nullptr 了,那么第二次的线程在来到一个 if 判断的时候,就直接退出了,不需要再加锁解锁,这样效率就提升了。

代码:

// 用 双重检测锁定 解决懒汉式多线程环境下线程不安全的问题
class TaskQueue {
public:
	TaskQueue(const TaskQueue& rhs) = delete;
	TaskQueue& operator = (const TaskQueue& rhs) = delete;

	static TaskQueue* get_instance() {
	    //外面再加一层判断
		if (m_task_queue == nullptr) {
		
			m_mutex.lock();
			if (m_task_queue == nullptr) {
				m_task_queue = new TaskQueue;
			}
			m_mutex.unlock();
			
		}
		return m_task_queue;
	}

	void print() {
		cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;
	}

private:
	TaskQueue() = default;
	static TaskQueue* m_task_queue;
	static mutex m_mutex;
};

TaskQueue* TaskQueue::m_task_queue = nullptr;
mutex TaskQueue::m_mutex;

实际上 双重检测锁定 的代码还是有问题的。

假设此时有两个线程 AB,线程 A 刚好要调用 m_task_queue = new TaskQueue; 这一句代码(假设此时 m_task_queue == nullptr);而线程 B 刚好来到第一个 if 判断。

	static TaskQueue* get_instance() {
	    //线程B 马上进入下面这个 if 判断
		if (m_task_queue == nullptr) {
		
			m_mutex.lock();
			if (m_task_queue == nullptr) {
			    //线程A 马上调用下面这一句代码
				m_task_queue = new TaskQueue;
			}
			m_mutex.unlock();
			
		}
		return m_task_queue;
	}

对于 m_task_queue = new TaskQueue; 创建对象的这一句代码,在底层实际上时会被分成三个步骤:

  • 第一步:分配内存用于存储 TaskQueue 对象;
  • 第二步:在分配的内存中构造一个 TaskQueue 对象(初始化内存);
  • 第三步:指针 m_task_queue 指向分配的内存;

由于编译器底层对我们的代码进行优化,就会将这些指令进行重排序,也就是打乱了它本来的步骤。

比如说将上述的步骤重排序之后,变成下面的:

  • 第一步:分配内存用于存储 TaskQueue 对象;
  • 第二步:指针 m_task_queue 指向分配的内存;
  • 第三步:在分配的内存中构造一个 TaskQueue 对象(初始化内存);

即 第二步 和 第三步 颠倒了顺序。

指令重排序在单线程下没有问题,在多线程下就有可能出现问题。

假设线程 A 此时刚好把前两步执行完了,m_task_queue 此时已经指向一块内存了,不过对这块内存进行操作是非法操作,因为创建对象还没有完成;线程 B 此时正好,进入第一个 if 判断,此时 m_task_queue 不为 nullptr,就直接退出,返回了没有构造完全的对象 m_task_queue

如果线程 B 对这个对象进行操作,就会出问题。

解决方法三:双重检测锁定 + 原子变量 (效率更低)

C++ 11 引入了 原子变量 atomic 可以解决 双重检测锁定 的问题。

代码:

// 用 原子变量 解决双重检测 的问题
class TaskQueue {
public:
	TaskQueue(const TaskQueue& rhs) = delete;
	TaskQueue& operator = (const TaskQueue& rhs) = delete;

	static TaskQueue* get_instance() {
		TaskQueue* task_queue = m_task_queue.load();
		if (task_queue == nullptr) {
			m_mutex.lock();
			task_queue = m_task_queue.load();

			if (task_queue == nullptr) {

				task_queue = new TaskQueue;
				m_task_queue.store(task_queue);

			}

			m_mutex.unlock();
		}
		return task_queue;
	}

	void print() {
		cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;
	}

private:
	TaskQueue() = default;
	//static TaskQueue* m_task_queue;
	static atomic<TaskQueue*> m_task_queue;
	static mutex m_mutex;
};

//TaskQueue* TaskQueue::m_task_queue = nullptr;
atomic<TaskQueue*> TaskQueue::m_task_queue;
mutex TaskQueue::m_mutex;

上面代码中使用原子变量 atomicstore() 函数来存储单例对象,使用 load() 函数来加载单例对象。

在原子变量中这两个函数在处理指令的时候默认的原子顺序是 memory_order_seq_cst即顺序原子操作 - sequentially consistent),这样也就避免了之前的指令重排的问题,使用顺序约束原子操作库,整个函数执行都将保证顺序执行,并且不会出现数据竞态(data races),缺点就是使用这种方法实现的懒汉模式的单例执行效率更低一些

解决方法四:静态局部变量(推荐)

在 C++ 11 直接使用 静态局部变量 在多线程环境下是不会出现问题的。

代码:

class TaskQueue
{
public:
    // = delete 代表函数禁用, 也可以将其访问权限设置为私有
    TaskQueue(const TaskQueue& rhs) = delete;
    TaskQueue& operator=(const TaskQueue& rhs) = delete;
    static TaskQueue* getInstance()
    {
        static TaskQueue task_queue;
        return &task_queue;
    }
    void print()
    {
        cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;
    }

private:
    TaskQueue() = default;
};

int main()
{
    TaskQueue* queue = TaskQueue::getInstance();
    queue->print();
    return 0;
}

之所以上面代码是线程安全的 ,是因为 C++ 11 规定了,并且这个操作是在编译时由编译器保证的:

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

三、总结

  • 懒汉模式的缺点是在创建实例对象的时候有安全问题,但这样可以减少内存的浪费(如果用不到就不去申请内存了)。
  • 饿汉模式则相反,在我们不需要这个实例对象的时候,它已经被创建出来,占用了一块内存。

四、练习

实现一个 任务队列。生产者线程生产任务加入任务队列;消费者线程取出任务队列的任务执行。

类成员:

  • 存储任务的容器,我们直接使用 STL 中的容器 queue
  • 互斥锁(mutex),在多线程访问的情况下,用于保护共享数据;

成员函数:

  • 判断任务队列是否为空;
  • 往任务队列中添加一个任务;
  • 往任务队列总删除一个任务;
  • 从任务队列中取出一个任务

为了简单起见,我们用一个 int 数,表示一个任务。

代码:

#if 1
// 用局部静态变量饿汉式单例 实现任务队列

class TaskQueue {
public:
	TaskQueue(const TaskQueue& rhs) = delete;
	TaskQueue& operator = (const TaskQueue& rhs) = delete;

	static TaskQueue* get_instance() {
		static TaskQueue task_queue;
		return &task_queue;
	}

	//判断任务队列是否为空
	bool is_empty() {
		lock_guard<mutex> locker(m_mutex);
		return q.empty();
	}

	//删除任务
	bool delete_task() {
		lock_guard<mutex> locker(m_mutex);
		if (q.empty()) return false;
		q.pop();
		return true;
	}

	//取出任务 (不删除任务)
	int take_task() {
		lock_guard<mutex> locker(m_mutex);
		if (q.empty()) return -1;
		return q.front();
	}

	//添加任务
	void add_task(int task) {
		lock_guard<mutex> locker(m_mutex);
		q.push(task);
	}

private:
	TaskQueue() = default;
	queue<int> q;
	mutex m_mutex;
};

#endif




int main() {
	TaskQueue* task_queue = TaskQueue::get_instance();

	thread t1([=]() {
		//生产者 t1 给任务队列添加10个任务
		for (int i = 0; i < 10; i++) {
			int task = i + 100;
			task_queue->add_task(task);
			cout << "producer thread produce a task : " << task << " , thread id is " << this_thread::get_id() << endl;
			this_thread::sleep_for(chrono::milliseconds(500));
		}
		});

	thread t2([=](){
		//让生产者线程先执行 保证先有任务
		this_thread::sleep_for(chrono::milliseconds(500));
		while (!task_queue->is_empty()) {
			int task = task_queue->take_task();
			task_queue->delete_task();
			cout << "consumer thread consume a task : " << task << " , thread id is " << this_thread::get_id() << endl;
			this_thread::sleep_for(chrono::milliseconds(1000));
		}
		});
	
	t1.join();
	t2.join();

	return 0;
}

3.参考

本篇博客是对于 :单例模式 的整理。文章来源地址https://www.toymoban.com/news/detail-696866.html

  • C++ 单例模式总结(5种单例实现方法)

到了这里,关于单例模式——C++版本的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

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

    一、概述         单例模式也称单态模式,是一种创建型模式,用于创建只能产生一个对象实例的类。例如,项目中只存在一个声音管理系统、一个配置系统、一个文件管理系统、一个日志系统等,甚至如果吧整个Windows操作系统看成一个项目,那么其中只存在一个任务管理

    2024年02月14日
    浏览(49)
  • C++特殊类设计(单例模式)

    C++98 将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。 原因: 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝了 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义

    2024年01月19日
    浏览(45)
  • 【C++】特殊类设计(单例模式)

    设计模式是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。 使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 根本原因是为了代码复用,增加可维护性。 设计模式的例子:迭代器模式 拷贝一共就只有两个场景,一

    2023年04月22日
    浏览(56)
  • 【C++】特殊类设计+单例模式+类型转换

    需要云服务器等云产品来学习Linux的同学可以移步/--腾讯云--/--阿里云--/--华为云--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。   目录 一、设计一个类,不能被拷贝 1、C++98 2、C++11 二、设计一个类,只能在堆上创建对象 1、将构造设为私有 2、将析构设为

    2024年02月06日
    浏览(54)
  • 【C++高阶(八)】单例模式&特殊类的设计

    💓博主CSDN主页:杭电码农-NEO💓   ⏩专栏分类:C++从入门到精通⏪   🚚代码仓库:NEO的学习日记🚚   🌹关注我🫵带你学习C++   🔝🔝 在实际场景中,总会遇见一些特殊情况, 比如设计一个类,只能在堆上开辟空间, 亦或者是设计一个类只能实例化一个对象 在实际需求的场景

    2024年02月04日
    浏览(47)
  • C++面试:单例模式、工厂模式等简单的设计模式 & 创建型、结构型、行为型设计模式的应用技巧

            理解和能够实现基本的设计模式是非常重要的。这里,我们将探讨两种常见的设计模式:单例模式和工厂模式,并提供一些面试准备的建议。 目录 单例模式 (Singleton Pattern) 工厂模式 (Factory Pattern) 面试准备  1. 理解设计模式的基本概念 2. 掌握实现细节 3. 讨论优缺

    2024年02月01日
    浏览(67)
  • 实际开发中常用的设计模式--------单例模式(知识跟业务场景结合)-----小白也能看懂(通俗易懂版本)

    1.定义 单例模式是一种创建型设计模式,它通过使用私有构造函数和静态方法来确保一个类只有一个实例,并且提供全局访问点来获取该实例。 通过使用单例模式,我们可以方便地管理全局唯一的对象实例,并且避免了多次创建相同类型的对象所带来的资源浪费问题 2.业务场

    2024年02月12日
    浏览(44)
  • 线程安全版本的单例设计模式 与 生产者消费者模型简介

    目录 单例设计模式 单例设计模式——饿汉式 单例设计模式——懒汉式 单例设计模式——懒汉式(优化步骤) 生产者消费者模型 介绍 优点 补充:关于阻塞队列 单例设计模式能够保证 某个类的实例在程序运行过程中始终都只会存在一份 。这一点在很多场景上都有需要,比

    2023年04月24日
    浏览(61)
  • C++中特殊类的设计与单例模式的简易实现

    对于这种特殊类的设计我们一般都是优先考虑私有构造函数。 然后对于一些特殊要求就直接通过静态成员函数的实现来完成。  这里选择禁掉拷贝构造函数和拷贝函数是为了防止将已创建的对象去拷贝构造新的对象。  这里如果没有禁掉operator new和operator delete的话就会导致以

    2024年01月18日
    浏览(47)
  • 从C语言到C++_37(特殊类设计和C++类型转换)单例模式

    目录 1. 特殊类设计 1.1 不能被拷贝的类 1.2 只能在堆上创建的类 1.3 只能在栈上创建的类 1.4 不能被继承的类 1.5 只能创建一个对象的类(单例模式)(重点) 1.5.1 饿汉模式 1.5.2 懒汉模式 2. 类型转换 2.1 static_cast 2.2 reinterpret_cast 2.3 const_cast 2.4 dynamic_cast 3. RTTI(了解)和类型转换常见面

    2024年02月10日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包