单例模式应该是最常用的设计模式了,也很容易理解。但是这里面却有一些坑。
游戏当中需要很多游戏配置,这个配置只需要一个实例,就可以采用单例模式。
饿汉模式需要注意一个点,那就是对象创建不要在头文件中,而是应该放在.cpp文件中,否则会在链接时冲突。
比如在instanc.hpp
中使用饿汉模式创建对象,在test.cpp
和test1.cpp
中进行包含这个头文件,此时单例模式就会因为有两个.cpp
文件都存在,这两个文件中就会因为存在同一个同名对象而造成链接冲突。
通常饿汉模式对象的创建在main函数之后,是非常安全的。
饿汉模式通常都是进行双重nullptr判断,然后new对象:
在多线程的情况下,m_instance = new GameConfig();
这一行代码通常分为三步:1.先调用malloc
分配内存,2.执行GameConfig
的构造函数来初始化内存,3.将m_instance 指向这一块内存。
但是在CPU内部执行过程中,很有可能因为编译器的优化打乱上面的三个步骤,也就是指令重排。
这也就意味着,线程1有可能先将m_instance 赋值,然后再调用构造函数初始化内存,那线程2拿到的就是一个没有被初始化的m_instance 。
关于指令重排的内容可以查看这一篇文章什么是指令重排序和内存屏障,看完你就懂了。
要解决指令重排,方法有很多。最简单的方法就是,在main函数执行开始,就先执行一次getInstance
。
另一种则是使用C++11的内存屏障:
#include
#include
#include
using namespace std;
namespace hjl_project1
{
class 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)
{
lock_guard<mutex> gcguard(my_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (m_instance == nullptr)
{
tmp = new GameConfig;
std::atomic_thread_fence(std::memory_order_release); //释放内存屏障
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return m_instance;
}
private:
GameConfig() {}
GameConfig(const GameConfig &tmpobj);
GameConfig &operator=(const GameConfig &tmpobj);
~GameConfig() {}
static atomic<GameConfig *> m_instance;
static mutex my_mutex;
};
std::atomic<GameConfig *> GameConfig::m_instance;
mutex GameConfig::my_mutex;
}
int main()
{
using namespace hjl_project1;
GameConfig *g_gc = GameConfig::getInstance();
}
最后一种解决办法,就是利用C++11 static 特性:如果当变量在初始化的时候,并发同时进⼊声明语句,并发线程将会阻塞等待初始化结束。
这也是最简洁的一种实现方式:
namespace hjl_project2
{
class GameConfig
{
public:
static GameConfig &getInstance()
{
static GameConfig instance;
return instance;
}
private:
GameConfig() {}
GameConfig(const GameConfig &tmpobj);
GameConfig &operator=(const GameConfig &tmpobj);
~GameConfig() {}
};
}
int main()
{
using namespace hjl_project2;
GameConfig &g_gc = GameConfig::getInstance();
}
这个版本具有以下优势
如果两个单例模式之间相互引用,则程序结束析构时,析构顺序并不确定,比如现在有两个单例Log、GameConfig,如果GameConfig的析构函数中需要利用Log对象记录一些信息,但是Log比GameConfig提前析构,此时就会出现问题,导致代码执行出错。
要解决这个问题,就要注意不要在单例模式的析构函数中,引用其他单例模式。