C++的单例模式的几种实现方式解析

棉花糖
发布于 2020-9-10 11:23
浏览
0收藏

单例模式有两种实现模式:


1)懒汉模式: 就是说当你第一次使用时才创建一个唯一的实例对象,从而实现延迟加载的效果。
2)饿汉模式: 就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。

 

所以,从实现手法上看,懒汉模式是在第一次使用单例对象时才完成初始化工作。因为此时可能存在多线程竞态环境,如不加锁限制会导致重复构造或构造不完全问题。

 

饿汉模式则是利用外部变量,在进入程序入口函数之前就完成单例对象的初始化工作,此时是单线程所以不会存在多线程的竞态环境,故而无需加锁。

 

以下是典型的几种实现

 

一、 懒汉模式,标准的 ”双检锁“ + ”自动回收“ 实现

 

class Singleton
{
public:
    static Singleton* GetInstance()
    {
        if (m_pInstance == NULL )
        {
            Lock(); // 加锁
            if (m_pInstance == NULL )
            {
                m_pInstance = new Singleton ();
            }
            UnLock(); // 解锁
        }
        return m_pInstance;
    }
 
    // 实现一个内嵌垃圾回收类    
    class CGarbo 
    {
    public:
        ~CGarbo()
        {
            if(Singleton::m_pInstance) 
                delete Singleton::m_pInstance;
        }
    };
 
    static CGarbo Garbo; // 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
 
private:
    Singleton(){};
    Singleton(Singleton const&); 
    Singleton& operator=(Singleton const&); 
 
    static Singleton* m_pInstance;
};
 
Singleton* Singleton::m_pInstance = NULL;
Singleton::CGarbo Garbo;

 

二、静态局部变量的懒汉模式 ,而不是new在堆上创建对象,避免自己回收资源。


这里仍然要注意的是局部变量初始化的线程安全性问题,在C++0X以后,要求编译器保证静态变量初始化的线程安全性,可以不加锁。但C++ 0X以前,仍需要加

锁。

 

class Singleton
{
public:
    static Singleton* GetInstance()
    {
        Lock(); // not needed after C++0x 
        static Singleton instance;  
        UnLock(); // not needed after C++0x 
 
        return &instance;
    }
 
private:
    Singleton() {};
    Singleton(const Singleton &);
    Singleton & operator = (const Singleton &);
};

 

在懒汉模式里,如果大量并发线程获取单例对象,在进行频繁加锁解锁操作时,必然导致效率低下。


三、饿汉模式,基础版本

 

因为程序一开始就完成了单例对象的初始化,所以后续不再需要考虑多线程安全性问题,就可以避免懒汉模式里频繁加锁解锁带来的开销。

 

class Singleton
{
public:
 
    static Singleton* GetInstance()
    {
        return &m_instance;
    }
 
private:
    Singleton(){};
    Singleton(Singleton const&); 
    Singleton& operator=(Singleton const&); 
 
    static Singleton m_instance;
};
 
Singleton Singleton::m_instance;  // 在程序入口之前就完成单例对象的初始化

 

虽然这种实现在一定程度下能良好工作,但是在某些情况下会带来问题 --- 就是在C++中 ”非局部静态对象“ 的 ”初始化“ 顺序 的 ”不确定性“, 参见Effective c++ 条款47。


考虑: 如果有两个这样的单例类,将分别生成单例对象A, 单例对象B. 它们分别定义在不同的编译单元(cpp中), 而A的初始化依赖于B 【 即A的构造函数中要调用B::GetInstance() ,而此时B::m_instance 可能还未初始化,显然调用结果就是非法的 】, 所以说只有B在A之前完成初始化程序才能正确运行,而这种跨编译单元的初始化顺序编译器是无法保证的。

 

四、饿汉模式,增强版本(boost实现)

 

在前面的方案中:饿汉模式中,使用到了类静态成员变量,但是遇到了初始化顺序的问题; 懒汉模式中,使用到了静态局部变量,但是存在着线程安全等问题。

 

boost 的实现方式是:单例对象作为静态局部变量,然后增加一个辅助类,并声明一个该辅助类的类静态成员变量,在该辅助类的构造函数中,初始化单例对象。以下为代码

 

class Singleton
{
public:
    static Singleton* GetInstance()
    {
        static Singleton instance;
        return &instance;
    }
 
protected:
    // 辅助代理类
    struct Object_Creator
    {
        Object_Creator()
        {
            Singleton::GetInstance();
        }
    };
    static Object_Creator _object_creator;
 
    Singleton() {}
    ~Singleton() {}
};
 
Singleton::Object_Creator Singleton::_object_creator;

 

首先,代理类这个外部变量初始化时,在其构造函数内部调用Singleton::GetInstance();从而间接完成单例对象的初始化,这就通过该代理类实现了饿汉模式的特性。


其次,仍然考虑第三种模式的缺陷。 当A的初始化依赖于B,【 即A的构造函数中要调用B::GetInstance() ,而此时B::m_instance 可能还未初始化,显然调用结果就是非法的 】 现在就变为【在A的构造函数中要调用B::GetInstance() ,如果B尚未初始化,就会引发B的初始化】,所以在不同编译单元内全局变量的初始化顺序不定的问题就随之解决。

 

最后,关于使用懒汉还是饿汉模式,我的理解:

 

如果这个单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,也是一种资源浪费吧。 所以这种情况懒汉模式(延迟加载)更好。

 

如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。

 

 

 

作者:simanstar

来源:CSDN

分类
收藏
回复
举报
回复