vlambda博客
学习文章列表

领悟设计模式---单例模式

 认真思考,真的领悟,仅仅是开始。



01


何为单例,何时使用单例



        援引某经典中是这样说的"保证一个类只有一个实例,并提供一个访问它的全局访问点"。这句话核心无非两点:其一实例唯一,其二在任何位置都可以获取到这个唯一实例。这不就是全局变量吗?似乎可以这么理解,但是面向对象的项目中一般是不允许使用全局变量的,既不符合面向对象的封装原则,而且存在安全隐患,这时单例模式的出现就很好的解决了这个问题。

        那么什么时候使用单例模式呢?其实上面的介绍中已经给了答案,这里仅列举几个简单的应用场景,读者自行体会即可。

  • 场景1:

      设备管理器,系统中有多个设备,但是只有一个设备管理器,用于管理设备。

  • 场景2:

    配置管理器,将配置数据缓存到程序后,可能一处或多处读写这一份数据。

  • 场景3:

    日志模块,我们开发项目都会有日志模块,这也是单例模式应用的场景之一。



02




单例要点



        单例的要点也无非是围绕怎么保证单例类只有一个实例,怎么提供一个全局的访问点,以及怎么保证使用单例是安全的。个人理解为如下几点:

  • 一个指向本类的静态指针

  • 一个返回本类实例的静态接口

  • 私有化的构造函数

  • 禁用赋值和拷贝

  • 单例的线程安全



03




单例的实现方式



        总的来说,单例的实现方式有懒汉式和饿汉式两种,何为懒汉式呢?何为饿汉式呢?

    • 懒汉式单例:就是在第一次用到类实例的时候才创建实例。

    • 饿汉式单例:就是在单例类定义的时候就实例化。

我习惯将其分为三种,分别是常规懒汉式单例,Magic Static懒汉式单例,饿汉式单例。下面通过具体代码示例来演示这三种单例方式,如有错误之处还请及时指正小汪。

  • 常规懒汉式单例:

头文件代码:

/* 常规懒汉单例模式 * 1.线程安全 * 2.单例资源自动释放 * 3.双检锁避免频繁加锁造成过大开销 * */class CSingletonLazy{ public: /* 禁用拷贝和赋值 */ CSingletonLazy(CSingletonLazy&)=delete; CSingletonLazy& operator=(const CSingletonLazy&)=delete; /* 返回实例的静态函数 */ static CSingletonLazy* get_instance(); private: /* 私有化的构造函数 */ CSingletonLazy(); /* 析构函数 */ ~CSingletonLazy(); private: /* 单例模式自动释放内存辅助类 */ class GCSingleton { public : ~GCSingleton() { if (m_pInstance != nullptr) { delete m_pInstance; m_pInstance = nullptr; } } }; /* 指向本类的实例指针 */ static CSingletonLazy* m_pInstance; /* 保证线程安全的锁 */ static std::mutex m_stMutex; /* 单例资源辅助回收 */ static GCSingleton m_stGc;};

源文件代码:

#include "design_mode_singleton.h"#include <stdio.h>CSingletonLazy* CSingletonLazy::m_pInstance = nullptr;std::mutex CSingletonLazy::m_stMutex;CSingletonLazy::GCSingleton CSingletonLazy::m_stGc;CSingletonLazy::CSingletonLazy(){ printf("CSingletonLazy::CSingletonLazy is called.\n");}CSingletonLazy::~CSingletonLazy(){ printf("CSingletonLazy::~CSingletonLazy is called.\n");}CSingletonLazy* CSingletonLazy::get_instance(){ /* 这里使用双检锁,只有判断指针为空的时候才加锁, * 避免每次调用 get_instance 的方法都加锁, * 毕竟锁的开销还是有的 */ if (m_pInstance == nullptr) { std::lock_guard<std::mutex> lk(m_stMutex); if(m_pInstance == nullptr) { m_pInstance = new CSingletonLazy(); } } return m_pInstance;}

main.cpp文件:

#include <stdio.h>#include "design_mode_singleton.h"int main(int argc, char **argv){ CSingletonLazy* pInstance1 = CSingletonLazy::get_instance();    CSingletonLazy* pInstance2 = CSingletonLazy::get_instance(); if (pInstance1 == nullptr) { printf("pInstance1 is nullptr\n");    } if (pInstance2 == nullptr) { printf("pInstance1 is nullptr\n");    } if (pInstance1 == pInstance2) { printf("pInstance1 = pInstance2\n");    } return 0;}
我们看下运行结果:
领悟设计模式---单例模式
构造函数执行了一次,且pInstance1=pInstance2,符合我们的预期。
针对常规式懒汉主要说明两点,第一,保证线程安全时使用了双检锁,避免每次调用时额外的锁开销;第二,这个单例借助辅助类实现资源自动回收,程序正常结束时,系统会调用静态成员变量static GCSingleton m_stGc的析构函数,在GCSingleton析构函数中释放了单例资源,通过上面运行log,确实自动释放了单例资源。 其实自动释放单例资源我们也可以借助智能指针来实现,因为C11标准已经支持了智能指针,我们只需要改变一下指向本类实例的静态指针的定义即可std::shared_ptr<CSingletonLazy> m_pInstance
  • Magic Static懒汉式单例:


上面的常规懒汉式单例貌似没什么大问题,是的没什么大问题。但是据说某些平台下双检锁会失效(备注:可能与编译期和指令集架构有关,没亲自验证过),如果双检锁失效,那每次调用单例都加锁,那是非常可怕的事情,高并发的情况会严重影响性能,别慌,Magic Static懒汉式单例可以解决。我们先看下代码:
/* Magic Static懒汉单例模式 * 1.利用静态局部变量的生存周期,实现单例资源自动回收 * 2.利用静态变量初始化时,线程安全特性避免加锁 * 3.简洁高效 * */class CSingletonMagicLazy{ public: /* 禁用拷贝和赋值 */ CSingletonMagicLazy(CSingletonMagicLazy&)=delete;        CSingletonMagicLazy& operator=(const CSingletonMagicLazy&)=delete; /* 返回实例的静态函数 */ static CSingletonMagicLazy* get_instance() { static CSingletonMagicLazy stInstance; return &stInstance; } private: /* 私有化的构造函数 */ CSingletonMagicLazy() { printf("CSingletonMagicLazy::CSingletonMagicLazy is called.\n");        } /* 析构函数 */ ~CSingletonMagicLazy() { printf("CSingletonMagicLazy::~CSingletonMagicLazy is called.\n"); }};
为什么这么简单一段代码就可以达到和上述那么多代码一样的效果呢?我们看看《Effective C++》中的描述:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。
也就是创建单例时,静态变量初始化是线程安全的,而且静态变量在生存周期结束时,会自动释放单例资源。我们同样按照之前main.cpp中的main函数,运行下程序,运行结果是一样的。

领悟设计模式---单例模式

  • 饿汉式单例模式:

头文件:

/* 饿汉单例模式 */class CSingletonHungry{ public: /* 禁用拷贝和赋值 */ CSingletonHungry(CSingletonHungry&)=delete; CSingletonHungry& operator=(const CSingletonHungry&)=delete; /* 返回实例的静态函数 */ static CSingletonHungry* get_instance(); private: /* 私有化的构造函数 */ CSingletonHungry(); /* 析构函数 */        ~CSingletonHungry(); /* 指向本类的实例指针 */ static CSingletonHungry m_stInstance;};

源文件:

CSingletonHungry CSingletonHungry::m_stInstance;CSingletonHungry::CSingletonHungry(){ printf("CSingletonHungry::CSingletonHungry is called.\n");}CSingletonHungry::~CSingletonHungry(){ printf("CSingletonHungry::~CSingletonHungry is called.\n");}CSingletonHungry* CSingletonHungry::get_instance(){ return &m_stInstance;}

运行结果:

领悟设计模式---单例模式



04




单例模板


    

    项目协同开发时可能好多模块和好多个像小汪一样的程序员都要使用单例模式,那么每个人都写一个自己的单例吗?显然不好,每个人写的都不一样,还容易出现Bug。本节的主题就是写一个单例模板,我们先看一下代码,然后再对单例模板做总结。

  • 基础模板类:

/* 父类单例模板 */template<typename T>class SingletonTemplate{ public: /* 返回实例的静态函数 */ static T* get_instance() { static T stInstance; return &stInstance;        } SingletonTemplate(const SingletonTemplate&)=delete; SingletonTemplate& operator =(const SingletonTemplate&)=delete; protected: /* 此处的构造函数是protected,因为子类需要继承 */ SingletonTemplate(){ printf("SingletonTemplate::SingletonTemplate is called.\n"); } /* 父类析构函数,使用虚函数,防止内存泄漏 */ virtual ~SingletonTemplate(){ printf("SingletonTemplate::~SingletonTemplate is called.\n"); }};
/* 子类继承父类 */class DerivedSingle : public SingletonTemplate<DerivedSingle>{ /* 需要将模板类声明为友元函数 * 否则无法调用子类私有的构造函数 */ friend class SingletonTemplate<DerivedSingle>; public: DerivedSingle(const DerivedSingle&)=delete;        DerivedSingle& operator =(const DerivedSingle&)= delete; private: DerivedSingle() { printf("DerivedSingle::DerivedSingle is called.\n"); } ~DerivedSingle(){ printf("DerivedSingle::~DerivedSingle is called.\n"); }};

main.cpp

 DerivedSingle* pInstance7 = DerivedSingle::get_instance(); DerivedSingle* pInstance8 = DerivedSingle::get_instance(); if (pInstance7 == nullptr) { printf("pInstance7 is nullptr\n"); }
if (pInstance8 == nullptr) { printf("pInstance8 is nullptr\n"); }
if (pInstance7 == pInstance8) { printf("pInstance7 = pInstance8\n"); }

执行结果:

领悟设计模式---单例模式

实现一个单例模板,主要有两点,一是基类的构造函数需要是protected,这样子类才可以继承;二是子类需要将自己作为模板参数T 传递给 SingletonTemplate<T> 模板,并且需要将基类声明为友元类,这样基类才能调用子类的私有构造函数。

  • 改进模板类:

/* 改进模板类 */template <typename T>class SingletonImprove {public: /* 返回实例的静态函数 */ static T* get_instance() noexcept(std::is_nothrow_constructible<T>::value){ static Token token; /* 函数静态变量可以实现延时构造 */ static T instance(token); return &instance; } /* 禁用拷贝和赋值 */ SingletonImprove(const SingletonImprove&) = delete; SingletonImprove& operator=(const SingletonImprove&) = delete;
protected: /* 只有子类才能获取令牌 */ struct Token{};
/* 构造和析构函数私有化 */ SingletonImprove() { printf("SingletonImprove::SingletonImprove is called.\n"); } virtual ~SingletonImprove() { printf("SingletonImprove::~SingletonImprove is called.\n"); }};
/* 改进模板类 - 子类 */class Single : public SingletonImprove<Single> {public: /* Token保证,父类需要调用,其他人无法调用 */ Single(Token token) { /* To want to do */ printf("Single::Single is called.\n"); } ~Single() { printf("Single::~Single is called.\n"); }
/* 禁用拷贝和赋值 */ Single(const Single&) = delete; Single& operator=(const Single&) = delete;};


main.cpp:

 Single* pInstance9 = Single::get_instance(); Single* pInstance10 = Single::get_instance(); if (pInstance9 == nullptr) { printf("pInstance9 is nullptr\n");    } if (pInstance10 == nullptr) { printf("pInstance8 is nullptr\n");    } if (pInstance9 == pInstance10) { printf("pInstance9 = pInstance10\n"); }

运行结果:

改进单例模板类使用了一个小技巧,在父类有一个protected Token,即使子类的构造函数是public,但是无法调用子类的构造函数额外构造对象,只能通过父类模板的get_instance接口来构造,父类保证子类实例唯一。

 


05




结语



上面已经非常全面的介绍了单例模式,下面总结两句使用心得:

  1. 对于单例的实现方式个人推荐使用Magic Static懒汉式的方式。

  2. 对于单例模板的实现方式个人推荐基础模板类,简单易懂。