C++设计模式_创建型模式_单件模式

本文记录单例模式。

单例模式又称为单例模式,是一种创建型模式,适用于产生一个对象的示例。

使用场景:项目中只存在一个对象,比如声音管理系统,一个配置系统,一个文件管理系统,一个日志系统,一个线程池等。

单件类的实现和特点

一个单例模式的实现。

cpp 复制代码
class GameConfig
{
private:
	GameConfig() {}
	GameConfig(const GameConfig&) {}
	GameConfig& operator=(const GameConfig&) {}

public:
	static GameConfig* getInstance()
	{
		if (_instance == NULL)
		{
			_instance = new GameConfig();
		}
		return _instance;
	}


private:
	static GameConfig* _instance;
};

GameConfig* GameConfig::_instance = nullptr;

void test()
{
	//GameConfig g1;		// 测试构造函数
	//GameConfig g2(g1);   // 测试拷贝构造函数	
	//GameConfig g3 = g1; // 测试拷贝构造函数
	//// 测试赋值运算符
	//GameConfig g4;
	//g4 = g1;
	// 测试单例
	GameConfig* g5 = GameConfig::getInstance();
	GameConfig* g6 = GameConfig::getInstance();
	if (g5 == g6)
	{
		cout << "g5 == g6" << endl;
	}
}

上面分别测试了单例类的构造函数,拷贝构造,拷贝赋值运算符。

单例模式的特点:

1 类的构造函数是私有的;

2 通过Public的静态成员函数getInstance() 创建单例类的对象;

3 将拷贝构造也设置为私有,保证对象可能被复制。

单件类在多线程中可能导致的问题

假如在多线程场景下使用单例模式:当线程1执行完if (instance == nullptr)这句话时,还未new GameConfig()对象,此时由于操作系统调度,切换到了线程2,此时线程2也会进入if (instance == nullptr),这样就可能导致了在多线程情况下,创建了两个单例类对象,这违背了开发者初衷,代码产生混乱。如何解决上面问题呢?

解决方式1:在if中加锁,代码如下所示。加锁方式解决了多个线程同时进入if条件的问题,但是随着而来的执行效率问题:一旦第一次创建成功后,后边即使多个线程也不会再执行_instance == nullptr的条件了,这种加锁的方式相当于对一个只读互斥量加锁,严重影响了程序执行效率。

cpp 复制代码
static GameConfig* getInstance()
{
	std::lock_guard<std::mutex> lock(_mutex);  // 
	if (_instance == NULL)
	{
		_instance = new GameConfig();
	}
	return _instance;
}

解决方式2:双重加锁。双重加锁代码如下,这种方式时为了提高多线程情况下效率,事实上这种方式确实减少了加锁的频率,提高的效率。这种方式在《C++并发编程实战》书上,作者很不赞同这种加锁方式,记录那个笔记时,再来看这种方式的缺点。

cpp 复制代码
static GameConfig* getInstance()
{
	if (_instance == nullptr)
	{
		std::lock_guard<std::mutex> lock(_mutex);  // 
		if (_instance == NULL)
		{
			_instance = new GameConfig();
		}
	}
	return _instance;
}

双重加锁代码的问题:内存访问重新排序,导致双重锁定失效的问题。上边的代码instance = new GameConfig(),这行代码大概分三个步骤完成:首先调用malloc分配内存,然后调用构造函数初始化这块内存,最后让instance 指向这块内存。但是,由于编译器优化等原因,上边的步骤被重新排序,执行顺序可能时132,这样麻烦就来了,因为instance指向这块new出来的内存,_instance == NULL就不成立了,表示已经new 成功了,可是这个new出来的内存并没有被初始化,当一个线程使用这个内存时,就会报错,这就是内存访问重新排序的问题。

实际使用时,先在main()中初始化,这样不必加锁也可实现线程的安全的单例模式。

书中提供的一个示例:

cpp 复制代码
class GameConfig
{
private:
	GameConfig() {};
	GameConfig(const GameConfig& tmpobj);
	GameConfig& operator = (const GameConfig& tmpobj);
	~GameConfig() {};
public:
	static GameConfig* getInstance()
	{
		GameConfig* tmp = m_instance.load(std::memory_order_relaxed);
		std::atomic_thread_fence(std::memory_order_acquire);
		if (tmp == nullptr)
		{
			std::lock_guard<std::mutex> lock(m_mutex);
			tmp = m_instance.load(std::memory_order_relaxed);
			if (tmp == nullptr)
			{
				tmp = new GameConfig();
				std::atomic_thread_fence(std::memory_order_release);
				m_instance.store(tmp, std::memory_order_relaxed);
			}
		}
		return tmp;
	}
private:
	static atomic<GameConfig*> m_instance;
	static std::mutex m_mutex;
};
std::atomic<GameConfig*> GameConfig::m_instance;
std::mutex GameConfig::m_mutex;

饿汉式与懒汉式

懒汉式单例模式在调用getInstance()时构造对象;

饿汉式单例模式在编译时候就构造对象;

饿汉式 单例模式

cpp 复制代码
// 饿汉式 单例模式
class GameConfig
{
private:
	GameConfig() {};
	GameConfig(const GameConfig& tmpobj);
	GameConfig& operator = (const GameConfig& tmpobj);
	~GameConfig() {};
public:
	static GameConfig* getInstance()
	{
		return m_instance;
	}

private:
	static GameConfig* m_instance;
private:
	class GC
	{
	public:
		~GC()
		{
			if (m_instance != nullptr)
			{
				delete m_instance;
				m_instance = nullptr;
			}
		}
	};
	static GC gc;
};
GameConfig* GameConfig::m_instance = new GameConfig();
GameConfig::GC GameConfig::gc;

懒汉式

cpp 复制代码
// 懒汉式单例模式另一种写法
class GameConfig
{
private:
	GameConfig() {};
	GameConfig(const GameConfig& tmpobj);
	GameConfig& operator = (const GameConfig& tmpobj);
	~GameConfig() {};

public:
	static GameConfig* getInstance()
	{
		return &m_instance;
	}
private:
	static GameConfig m_instance;
};
GameConfig GameConfig::m_instance;

单件类对象内存释放问题

上边的饿汉式单例模式中,在类中加了一个GC类,在GC类的析构函数中释放单例模式的对象。

单件类定义、UML图及另外一种实现方法

单件设计模式定义:保证一个类仅有一个实例存在,同时提供能对该实例访问的全局方法(getInstance成员函数)。

下面也是一种常见的单例模式写法。

cpp 复制代码
class GameConfig
{
private:
	GameConfig() {};
	GameConfig(const GameConfig& tmpobj);
	GameConfig& operator = (const GameConfig& tmpobj);
	~GameConfig() {};

public:
	static GameConfig& getInstance()
	{
		static GameConfig instance; // 局部静态变量,在运行时期初始化;
		return instance;
	}
};
void test()
{
	static int a = 100;  // 编译时期初始化

	GameConfig& g1 = GameConfig::getInstance();
}

单例模式UML

类和类之间是聚合关系。

相关推荐
笨手笨脚の2 小时前
设计模式-享元模式
设计模式·享元模式·结构型设计模式·设计模式之美
风语者日志2 小时前
创作者模式—单例设计模式
设计模式
舒克起飞了2 小时前
设计模式——单例模式
java·单例模式·设计模式
大飞pkz2 小时前
【设计模式】享元模式
开发语言·设计模式·c#·享元模式
茉莉玫瑰花茶2 小时前
C++扩展 --- 并发支持库(补充3)
开发语言·c++
半桔3 小时前
【网络编程】TCP 粘包处理:手动序列化反序列化与报头封装的完整方案
linux·网络·c++·网络协议·tcp/ip
GUIQU.5 小时前
【QT】嵌入式开发:从零开始,让硬件“活”起来的魔法之旅
java·数据库·c++·qt
西阳未落9 小时前
C++基础(21)——内存管理
开发语言·c++·面试
超级大福宝9 小时前
使用 LLVM 16.0.4 编译 MiBench 中的 patricia遇到的 rpc 库问题
c语言·c++