漫谈设计模式之单例模式(Singleton)

什么是单例模式?单例模式顾名思义就是确保一个类在内存中只有一份实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。


这时候有人会抬杠说我就用一个全局变量(类)不就也是一个单例,根本不需要设计模式。但是这样的代码不是很优雅的。使用全局变量是可以保证方便的访问实例,但是不能保证只有一个这个变量的实例---除了通过这个全局实例2外,还是可以创建该类的局部(本地)实例,这样其实就不是严格意义上的单例了!

看看GoF(四人帮)这些牛逼的大神们是怎么去实现一个单例吧。这些大神们通过类本身来管理实现严格意义上的单例。定义一个单例类,大神们定义了该类的私有静态指针,并提供一个公有的静态方法去获取该私有静态指针指向的实例。单例类Singleton在其公有静态成员函数中隐藏了创建实例的操作。业界习惯上把这个静态公有函数叫做Instance()或者getInstance(),该函数返回该类唯一实例的指针。单例模式就是通过这个公有静态成员函数的返回值去访问到内存实例的。


废话不多说,看代码:

#include <iostream>

using namespace std;

class Singleton
{
private:
    //构造函数私有化,防止被构造
    Singleton() {}
    //把复制构造函数和赋值操作符也设为私有,防止被复制
    Singleton(const Singleton&) {}
    Singleton operator=(const Singleton&) {}

    //定义私有静态指针给公有静态接口使用
    static Singleton* m_pInstance;

public:
    //公有静态接口供外部访问该单例
    static Singleton* getInstance() {
        if(NULL == m_pInstance) { //判断是否是第一次调用,第一次调用才分配内存,不是
                                  //第一次调用的话就不分配内存直接返回已经分配的内存的指针
            m_pInstance = new Singleton();
        }
        return m_pInstance;
    }

};


Singleton* Singleton::m_pInstance = NULL;


int main()
{
    Singleton* p1 = NULL;
    Singleton* p2 = NULL;
    p1 = Singleton::getInstance();
    p2 = Singleton::getInstance();


    cout<<"p1 address is:"<<p1<<endl;
    cout<<"p2 address is:"<<p2<<endl;
    cout<<"p1 == p2 :"<<static_cast<bool>(p1==p2)<<endl;

    return 0;
}

运行结果:


根据大神们总结的设计思路我们发现确实实现了一个很友好的单例类。用户访问唯一实例的方法只有getInstance()成员函数。如果不通过这个函数,任何创建实例的尝试都将失败,因为我们已经把构造函数,复制构造函数和赋值运算符重载函数都设置为private。getInstance()使用的的懒汉式初始化方式,也就是说它的返回值是当这个函数首次被访问时被创建的。这是一种防弹设计----所有getInstance()之后的调用都返回相同实例的指针:


Singleton* p1 = Singleton::getInstance();

Singleton* p2 = p1->getInstance();

Singleton& ref = *Single::getInstance();

对getInstance()稍加修改,这个设计模板就可以适应于可变多实例情况,如一个类允许最多五个实例。


到此先总结一下:

单例类Singleton有以下特征:

单例类有一个指向唯一实例的静态指针m_pInstance,并且是私有的;

单例类有一个公有的函数,通过这个公有的函数可以获取这个唯一的实例,并且在需要的时候创建该实例;

单例类的构造函数是私有的(最好把拷贝(复制)构造函数和赋值运算符重载函数也都搞成私有的),这样就不能从别处创建该类的实例。

大多数时候(注意不是所有的情况下),这样的实现都不会出现问题。但是单例的内存释放却是个问题!m_pInstance指向的内存空间什么时候释放?改实例的析构函数什么时候执行?如果在类的析构行为中有必须的操作,比如关闭文件,释放外部资源,那么上面的代码无法实现这个要求。

可以在程序结束时调用getInstance(),并对返回的指针调用delete操作。这样可以实现功能,但是很容易忘记通过这种方式去释放单例内存。

一个好的方法是让这个类自己知道在合适的时候把自己删除,或者说把删除自己的操作挂在操作系统中的某个合适的点上,使其在恰当的时候被自动执行。

我们知道,在程序结束的时候,系统会自动析构所有的全局变量。事实上,系统也会析构所有类的静态成员变量,就像这些静态成员也是全局变量一样(其实静态成员变量和全局变量都是存在内存的静态数据区)。利用这个特征,我们可以在单例类中定义一个这样的静态成员变量,而它唯一工作就是在析构函数中删除单例类中的实例。


代码如下:

class Singleton
{
private:
    //构造函数私有化,防止被构造
    Singleton() {}
    //把复制构造函数和赋值操作符也设为私有,防止被复制
    Singleton(const Singleton&) {}
    Singleton operator=(const Singleton&) {}

    //定义私有静态指针给公有静态接口使用
    static Singleton* m_pInstance;

    class GC   //垃圾回收类,它的唯一工作就是在析构函数中删除掉单例实例
    {
    public:
        ~GC()
        {
            if(Singleton::m_pInstance) {
                delete Singleton::m_pInstance;
            }
        }
    };
    static GC m_GC;    //定义一个静态成员变量,程序结束时系统会自动调用它的析构函数
public:
    //公有静态接口供外部访问该单例
    static Singleton* getInstance() {
        if(NULL == m_pInstance) { //判断是否是第一次调用,第一次调用才分配内存,不是
                                  //第一次调用的话就不分配内存直接返回已经分配的内存的指针
            m_pInstance = new Singleton();
        }
        return m_pInstance;
    }

};

类GC(garbage collector)被定义为Singleton的私有内嵌类,防止该类在其他地方被滥用。

程序运行结束时,系统会调用Singleton的静态成员m_GC的析构函数,该析构函数会删除单例的唯一实例。

使用这种方法释放单例对象有以下特征:

在单例类内部定义专有的嵌套类;

在单例类内定义私有的专门用于释放的静态成员;

利用程序在结束时析构全局变量(或静态变量)的特性,选择最终的释放时机;

使用单例的代码不需要任何操作,不必关心对象的释放(在不知不觉中就完成了单例内存的释放工作)。


上面讲的属于懒汉式单例模式,如果使用多线程会出现安全隐患,具体体现在当一个线程进入getInstance()的判断条件但是还没new的时候例外的线程也是可以进入判断条件的,这就导致了new出了多个单例对象。

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

class Singleton
{
private:
    //构造函数私有化,防止被构造
    Singleton() {}
    //把复制构造函数和赋值操作符也设为私有,防止被复制
    Singleton(const Singleton&) {}
    Singleton operator=(const Singleton&) {}

    //定义私有静态指针给公有静态接口使用
    static Singleton* m_pInstance;
    static mutex m_mutex;

    class GC   //垃圾回收类,它的唯一工作就是在析构函数中删除掉单例实例
    {
    public:
        ~GC()
        {
            if(Singleton::m_pInstance) {
                delete Singleton::m_pInstance;
            }
        }
    };
    static GC m_GC;    //定义一个静态成员变量,程序结束时系统会自动调用它的析构函数
public:
    //公有静态接口供外部访问该单例
    static Singleton* getInstance() {

        if(NULL == m_pInstance) { //判断是否是第一次调用,第一次调用才分配内存,不是
                                  //第一次调用的话就不分配内存直接返回已经分配的内存的指针
            std::lock_guard<std::mutex> lock(m_mutex);  //自解锁
            if(NULL == m_pInstance) {
                m_pInstance = new Singleton();
            }
        }
        return m_pInstance;
    }

};


Singleton* Singleton::m_pInstance = NULL;
mutex Singleton::m_mutex;

我们在getInstance()中采用了双检锁(DCL), 为什么要采用DCL(Double-Check-Lock)?

简单来说采用双检锁是为了性能,即使把第一个判断条件去掉,加锁线程同步之后还是会有一次判断,因此一个类只创建一个实例依然是有保障的。但是这样的写法会使得每个线程执行getInstance()方法的时候都必须获得一个锁,于是锁的获得和释放的开销(包括上下文切换、内存同步等开销)就无条件的存在。相反,如果在执行加锁代码块前先进行一次是否为NULL的判断,那么加锁代码块被多个线程执行到的几率就大大降低了(我们假设开了100个线程并且第一个线程就new了单例对象同时第三个线程还没进行第一次判断,这意味着只有第一个和第二个线程要进行加锁和解锁剩下的98个线程都是直接得到改单例对象的指针),因此锁的开销得以最大化降低。


接着我们在看看饿汉式单例模式:

饿汉式单例模式的特点是不用加锁,执行效率比较高。饿汉式单例是在类加载时(main函数执行前)就已经初始化了,浪费内存。


示例代码:

/**
 * @brief The Singleton class
 * 饿汉式单例模式
 */
class Singleton
{
private:
    //构造函数私有化,防止被构造
    Singleton() {}
    //把复制构造函数和赋值操作符也设为私有,防止被复制
    Singleton(const Singleton&) {}
    Singleton operator=(const Singleton&) {}

    //定义私有静态指针给公有静态接口使用
    static Singleton* m_pInstance;

    class GC   //垃圾回收类,它的唯一工作就是在析构函数中删除掉单例实例
    {
    public:
        ~GC()
        {
            if(Singleton::m_pInstance) {
                delete Singleton::m_pInstance;
            }
        }
    };
    static GC m_GC;    //定义一个静态成员变量,程序结束时系统会自动调用它的析构函数
public:
    //公有静态接口供外部访问该单例
    static Singleton* getInstance() {

        return m_pInstance;
    }

};


Singleton* Singleton::m_pInstance = new Singleton();


int main()
{
    Singleton* p1 = NULL;
    Singleton* p2 = NULL;
    p1 = Singleton::getInstance();
    p2 = Singleton::getInstance();


    cout<<"p1 address is:"<<p1<<endl;
    cout<<"p2 address is:"<<p2<<endl;
    cout<<"p1 == p2 :"<<static_cast<bool>(p1==p2)<<endl;

    return 0;
}


运行结果:



比较懒汉式和饿汉式单例的优缺点:

1、时间和空间:比较上面两种写法:懒汉式是典型的时间换空间,也就是每次获取内存实例都会进行判断,看是否需要创建单例,费判断的时间,当然,如果一直没有人使用的话,那就不会创建实例,节约内存空间。

饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要在判断了,节约了运行时间。

2、线程安全:从线程安全性上讲,不加锁的懒汉式线程是不安全的(可以通过加锁实现线程安全,DCL最佳),而饿汉式是线程安全的(因为饿汉式在main函数还没执行之前就已经完成了实例化了,不存在并发发生的可能性)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值