设计模式——Singleton

本文深入探讨了单例模式的设计理念及其实现方式,包括懒汉式和饿汉式的区别,着重解析了线程安全问题及解决方案,如双重检查锁定模式(DCLP),并讨论了单例对象的生命周期管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单例模式(Singleton Pattern)也是一种创建型模式。

这种模式的重点在于保证类只有一个实例

类通过维护一个static成员变量来记录这个唯一的对象实例,并且提供一个static成员函数作为获得这个唯一实例的接口。
因为类不可被实例化,因此我们将它的构造函数声明为受保护或私有。

还是看一个例子。
写一个类Singleton,这个类有一个静态成员变量m_singleton,是这个类的一个静态实例,以及一个静态成员函数getInstance(),来负责为外部提供这个实例。

class Singleton
{
    public:
        static Singleton *getInstance();
    protected:
        Singleton();
    private:
        static Singleton *m_singleton;
};

Singleton *Singleton::m_singleton = NULL;

Singleton::Singleton()
{
    //ctor
    cout<<"this is a constructor for Singleton"<<endl;
}

Singleton *Singleton::getInstance()
{
    if(m_singleton == NULL)
        m_singleton = new Singleton();
    return m_singleton;
}

进行实例化。
不能通过构造函数进行对象创建,因为构造函数声明为保护,对外不可见。
通过静态方法getInstance()来获取类的唯一可用对象。

//Singleton *singleton = new Singleton();
Singleton *singleton = Singleton::getInstance();

输出:

this is a constructor for Singleton

Singleton模式是作为全局变量的替代品出现的,因此它具有全局变量的特性:全局可见、全生命周期。但是它还有最重要的一个特性:同类型的对象实例唯一

上面的例子只是给出单例模式的一种实现方式,实际上会根据不同的情况有多种不同的处理。
下面总结一下:

根据类对象实例化的时间可以分为懒汉和饿汉两种实现方法。

1.懒汉模式:即不到使用该类对象的时候不进行实例化,也即只有在第一次调用类实例的时候才产生一个新的类实例对象,并且以后仅返回该对象。

我们上面采用的就是这种方式。
但是问题在于,这种实现方式是线程不安全的。当多个线程同时首次调用getInstance()方法,然后均检测到m_singleton指针为空,那么两个线程会同时去构造Singleton实例给m_singleton,这种情况要么导致错误,要么违背了单例模式只创建一个对象的原则。

那么线程不安全,应该怎么解决呢?两个方法。
此处给出伪代码,只是从理论上说明解决单例模式的线程安全可以采用的做法,实例验证后面再补充。

  • 加锁

最直观的方法就是在判断对象是否实例化之前进行加锁。

Singleton *Singleton::getInstance()
{
    lock();
    if(m_singleton == NULL)
        m_singleton = new Singleton();
    return m_singleton;
}

分析这种加锁方式,最大的问题就是昂贵的性能开销:每次访问获取实例的函数都要进行一次加锁操作。实际上,只有在对象初始化的时候需要加锁,后面的调用过程中的锁操作都是没有必要的。那么就出现了一种新的加锁方式——双重检查锁定模式(DCLP)。

看下DCLP的经典实现方法。

Singleton *Singleton::getInstance()
{
    //第一次检查
    if(m_singleton == NULL)
    {
        lock();
        //第二次检查
        if(m_singleton == NULL)
            m_singleton = new Singleton();
    }   
    return m_singleton;
}

这种方案会进行两次检查,初始化完成后,也即大部分时间,m_singleton都是非空的,因此没必要进行初始化,完成判断、加锁等操作。因此我们先做一次检查,判断是否完成了初始化,第二次检查,进行初始化过程中加锁操作。

对于这种方式,还存在质疑:在执行m_singleton = new Singleton();时,可能Singleton类还没有初始化完成,就已经为m_singleton分配地址了,然后就导致另一个调用getInstance()的线程,获取到还未完成初始化的对象指针,如果此时使用必然会导致错误。【这种问题导致的原因在后面单独说】

  • 静态对象

第二种方法就是通过一个静态实例来实现,返回时只需要返回指针。

Singleton *Singleton::getInstance()
{
    static Singleton singleton;
    return &singleton;
}

注意:C++0X以后,要求编译器保证内部静态变量的线程安全性,可以不加锁;但C++ 0X以前仍需要加锁。

2.饿汉模式:即不等到使用该类对象,而是在类被定义的时候就进行实例化。

看下代码实现。

class Singleton
{
    public:
        static Singleton *getInstance();
    protected:
        Singleton();
    private:
        static Singleton *m_singleton;
};

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

Singleton::Singleton()
{
    //ctor
    cout<<"this is a constructor for Singleton"<<endl;
}

Singleton *Singleton::getInstance()
{
    return m_singleton;
}

简单而且安全。
因为静态实例初始化,在程序开始时进入主函数之前就由主线程以单线程方式完成,因此不必担心多线程问题。

对比一下两种模式:

1、懒汉模式是在使用类实例的时候去进行实例化,而饿汉模式则在类定义的时候去进行实例化。
2、访问量较小的时候,采用懒汉实现,可以节省空间。懒汉模式需要用双重检查锁定(DCLP)来保证线程安全,但是锁操作会带来性能影响。
3、访问量较大时,或线程同步操作比较多时,采用饿汉实现,可以获得更好的性能。

然后说一下单例对象的析构。

最直接的方式是在使用完实例对象后,调用getInstance()并对返回的指针做delete操作。
这并不是最好的方式,且不说经常被遗忘,而且需要明确使用完实例的时间点。

最好的方式是,类自己完成实例对象的删除。
我们知道,程序结束时,会自动析构全局变量,包括类的静态成员变量。利用这一点,我们可以在单例类里面定义一个专门用来析构实例对象的静态成员变量。

定义一个私有的内嵌类GarColl和静态GarColl对象garcoll。
GarColl类有一个public的析构函数,用来删除实例对象指针。

class Singleton
{
    public:
        static Singleton *getInstance();
    protected:
        Singleton();
        ~Singleton();
    private:
        static Singleton *m_singleton;

        class GarColl
        {
            public:
                ~GarColl();
        };

        static GarColl garcoll;
};

......(省略其他的一些函数定义)

Singleton::GarColl Singleton::garcoll;

Singleton::GarColl::~GarColl()
{
    cout<<"this is a destructor for GarColl"<<endl;
    if(m_singleton)
        delete m_singleton;
}

注意:
1、GarColl是被定义为一个私有内嵌类,防止其他地方滥用析构。
2、GarColl的静态对象需要被初始化,类型为Singleton::GarColl,对象为Singleton::garcoll。garcoll本质是一个Singleton类静态对象。

最后运行结果为:

this is a constructor for Singleton
getInstance of Singleton
this is a destructor for GarColl
this is a destructor for Singleton

自动进行了静态对象的析构,进而完成了对单例对象的析构。

总结一下:

Singleton模式就是使得类仅有一个实例的对象创建模式
特点:
1、Singleton类只有一个类实例;
2、Singleton类自己完成唯一对象的创建;
3、Singleton类对外提供了一种访问其唯一的对象的方式。

Singleton类主要解决的问题是,统一一个被全局使用的类,并且控制实例个数,节省系统资源。

优点:
1、内存中只有一个类实例,节约系统资源,无需频繁地进行创建和销毁实例;
2、避免对某个单一资源多重占用。
缺点:
1、实例化过程中的多线程会导致安全问题;


还有一些没讲清楚的可以参考这一篇文章Singleton的深入


【DCLP存在的问题】

前面说DCLP的时候,我们提到一个问题:可能Singleton类还没有初始化完成,就已经为m_singleton分配地址,此时对象指针指向一个不完整的对象。

造成这种问题的原因是:程序的指令执行顺序不确定。

我们进行对象实例化m_singleton = new Singleton();时,实际上这条语句会被分为这样的三个步骤执行:

  • Step1:为对象开辟一块内存;
  • Step2:构造对象,填到这部分内存里;
  • Step3:将对象指针指向这块内存。

但是三个步骤在执行时,可能是先将指针指向内存,再往内存里面填对象。这时,一个线程在初始化时,完成了Step1,先去执行Step3,对象指针非空了,但是Step2还没完成,也即对象还没构建完;另一个线程不知道这一切,它只去判断了m_singleton 为非空,以为对象已经构建完了,直接返回。

所以我们想的解决方法就是加一个中间临时变量做过渡,保证指针被赋值时对象已经创建完成了。这样,代码就变成:

Singleton *Singleton::getInstance()
{
    //第一次检查
    if(m_singleton == NULL)
    {
        lock();
        //第二次检查
        if(m_singleton == NULL)
        {
            Singleton *pTmp = new Singleton();
            m_singleton = pTmp;
        }   
    }   
    return m_singleton;
}

但是这种情况,还是可能存在问题:编译器优化。

编译器会识别出pTmp是一个无用的变量,然后直接给优化了。另外,我们做双重检查的时候,编译器看到两个if判断,其中一个甚至都不执行,它可能出于某种考虑判断其中某个if判断是多余的,然后进行优化。

解决编译器的优化问题,可以使用volatile关键字,后面详细研究这个关键字的时候再说。
可以参照这篇文章C++和双重检查锁定模式(DCLP)的风险

这一篇单例模式就到此为止。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值