本文记录单例模式。
单例模式又称为单例模式,是一种创建型模式,适用于产生一个对象的示例。
使用场景:项目中只存在一个对象,比如声音管理系统,一个配置系统,一个文件管理系统,一个日志系统,一个线程池等。
单件类的实现和特点
一个单例模式的实现。
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
类和类之间是聚合关系。