C++设计模式_创建型模式_单件模式
本文记录单例模式。
单例模式又称为单例模式,是一种创建型模式,适用于产生一个对象的示例。
使用场景:项目中只存在一个对象,比如声音管理系统,一个配置系统,一个文件管理系统,一个日志系统,一个线程池等。
单件类的实现和特点
一个单例模式的实现。
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的条件了,这种加锁的方式相当于对一个只读互斥量加锁,严重影响了程序执行效率。
static GameConfig* getInstance()
{std::lock_guard<std::mutex> lock(_mutex); // if (_instance == NULL){_instance = new GameConfig();}return _instance;
}
解决方式2:双重加锁。双重加锁代码如下,这种方式时为了提高多线程情况下效率,事实上这种方式确实减少了加锁的频率,提高的效率。这种方式在《C++并发编程实战》书上,作者很不赞同这种加锁方式,记录那个笔记时,再来看这种方式的缺点。
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()中初始化,这样不必加锁也可实现线程的安全的单例模式。
书中提供的一个示例:
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()时构造对象;
饿汉式单例模式在编译时候就构造对象;
饿汉式 单例模式
// 饿汉式 单例模式
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;
懒汉式
// 懒汉式单例模式另一种写法
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成员函数)。
下面也是一种常见的单例模式写法。
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
类和类之间是聚合关系。