C++设计模式---单例模式

单例模式应该是最常用的设计模式了,也很容易理解。但是这里面却有一些坑。


单例模式的使用场景

游戏当中需要很多游戏配置,这个配置只需要一个实例,就可以采用单例模式。

单例模式注意的一些坑

饿汉模式

饿汉模式需要注意一个点,那就是对象创建不要在头文件中,而是应该放在.cpp文件中,否则会在链接时冲突。
比如在instanc.hpp中使用饿汉模式创建对象,在test.cpptest1.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 <iostream>
#include <mutex>
#include <atomic>
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();
}

这个版本具有以下优势

  1. 利⽤静态局部变量特性,延迟加载;
  2. 利⽤静态局部变量特性,系统⾃动回收内存,⾃动调⽤析构函数;
  3. 静态局部变量初始化时,没有 new 操作带来的cpu指令reorder操作;
  4. c++11 静态局部变量初始化时,具备线程安全;

单例模式之间相互引用的问题

如果两个单例模式之间相互引用,则程序结束析构时,析构顺序并不确定,比如现在有两个单例Log、GameConfig,如果GameConfig的析构函数中需要利用Log对象记录一些信息,但是Log比GameConfig提前析构,此时就会出现问题,导致代码执行出错。

要解决这个问题,就要注意不要在单例模式的析构函数中,引用其他单例模式

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

今天也要写bug、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值