单例模式——懒汉模式

上一篇文章讲到了单例模式的饿汉模式,这一篇来讨论一下单例模式的懒汉模式。

所谓懒汉模式,就是像一个懒汉一样,需要用到创建实例了程序再去创建实例,不需要创建实例程序就“懒得”去创建实例,这是一种时间换空间的做法,这体现了“懒汉的本性”。

如果有多个线程,同时调用了获取实例,那么可能就会产生多个实例,那么这就不是我们的单例模式了。所以我们需要做一些事情才能保证我们是一个单例类。

额当然,我们先来考虑是单线程的情况,再去考虑多线程的情况,我们总得先把懒汉模式的本质给弄明白。

对于饿汉模式来说,它在单例类内部定义了一个类对象的指针,在初始化这个指针的时候,直接调用new在堆上申请了空间。于是达到了这种效果,在进入main函数之前,这个单例类的实例已经创建好了。

而对于懒汉模式而言,我们可不可以不让这个类对象指针在初始化的时候就new,而是给它赋一个NULL,那这样,在进入main函数之前,这个类对象指针只是一个空指针,并没有产生实际的对象。而当我们在程序中调用getInstance函数时,就需要进行一个判断,如果该类对象指针为空,那么我们就需要调用new创建一个对象,而如果该类对象指针不为空,那么我们就不用创建对象直接返回该指针就好了。根据这个思路,我们实现了下面的代码(因为我们的静态成员变量采用的是类对象的指针而不是类对象,因此我们需要写一个垃圾回收机制,这里我们采用的是上一篇文章的方法二,即实现一个内部的垃圾回收类)

singleton.hpp

#pragma once
 
#include <iostream>
using namespace std;
 
class Singleton{
private:
    Singleton(){
        cout << "创建了一个单例对象" << endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        // 析构函数我们也需要声明成private的
        //因为我们想要这个实例在程序运行的整个过程中都存在
        //所以我们不允许实例自己主动调用析构函数释放对象
        cout << "销毁了一个单例对象" << endl;                                            
    }
         
    static Singleton* instance;  //这是我们的单例对象,它是一个类对象的指针
public:
    static Singleton* getInstance();
                                        
private:
    //定义一个内部类
    class Garbo{
    public:
        Garbo(){}
        ~Garbo(){
            if(instance != NULL){
                delete instance;
                instance = NULL;
            }
        }
    };

    //定义一个内部类的静态对象
    //当该对象销毁的时候,调用析构函数顺便销毁我们的单例对象
    static Garbo _garbo;
};

//下面这个静态成员变量在类加载的时候就已经初始化好了
Singleton* Singleton::instance = NULL; 
Singleton::Garbo Singleton::_garbo;     //还需要初始化一个垃圾清理的静态成员变量

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

test.cc

#include "singleton.hpp"

int main(){
    cout << "Now we get the instance" << endl;
    Singleton* instance1 = Singleton::getInstance();
    Singleton* instance2 = Singleton::getInstance();
    Singleton* instance3 = Singleton::getInstance();
    cout << "Now we destroy the instance" << endl;
    return 0;
}

程序的运行结果如下图

可以看到我们先进入了main函数中,在调用了第一个getInstance获取实例的时候才创建了一个单例对象。在调用后续的getInstance获取实例的时候因为instance不为空,所以直接返回instance。

当然了上面我们是在堆上创建了对象,想在栈上创建对象也是可以的

singleton.hpp

#pragma once
 
#include <iostream>
using namespace std;
 
class Singleton{
private:
    Singleton(){
        cout << "创建了一个单例对象" << endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        // 析构函数我们也需要声明成private的
        //因为我们想要这个实例在程序运行的整个过程中都存在
        //所以我们不允许实例自己主动调用析构函数释放对象
        cout << "销毁了一个单例对象" << endl;                                            
    }
         
public:
    static Singleton* getInstance();                                        
};

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

test.cc

#include "singleton.hpp"

int main(){
    cout << "Now we get the instance" << endl;
    Singleton* instance1 = Singleton::getInstance();
    Singleton* instance2 = Singleton::getInstance();
    Singleton* instance3 = Singleton::getInstance();
    cout << "Now we destroy the instance" << endl;
    return 0;
}

结果是一样的

上面这两种情况在单线程的情况下,是没有问题。但是试想在多线程的情况下,试想一下如果多个线程同时调用了getInstance函数,此时可能存在多线程竞态环境,就可能会产生重复构造或者是构造不完全等问题。因此我们在getInstance函数中还要采用线程同步的方式,保证线程同步,从而达到单例类的目的

singleton.hpp(在堆上创建对象)

#pragma once
 
#include <iostream>
using namespace std;
 
class Singleton{
private:
    Singleton(){
        cout << "创建了一个单例对象" << endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        // 析构函数我们也需要声明成private的
        //因为我们想要这个实例在程序运行的整个过程中都存在
        //所以我们不允许实例自己主动调用析构函数释放对象
        cout << "销毁了一个单例对象" << endl;                                            
    }
         
    static Singleton* instance;  //这是我们的单例对象,它是一个类对象的指针
public:
    static Singleton* getInstance();
                                        
private:
    //定义一个内部类
    class Garbo{
    public:
        Garbo(){}
        ~Garbo(){
            if(instance != NULL){
                delete instance;
                instance = NULL;
            }
        }
    };

    //定义一个内部类的静态对象
    //当该对象销毁的时候,调用析构函数顺便销毁我们的单例对象
    static Garbo _garbo;
};

//下面这个静态成员变量在类加载的时候就已经初始化好了
Singleton* Singleton::instance = NULL; 
Singleton::Garbo Singleton::_garbo;     //还需要初始化一个垃圾清理的静态成员变量

Singleton* Singleton::getInstance(){
    Lock(); // 加锁
    if(instance == NULL)
        instance = new Singleton();
    UnLock(); // 解锁

    return instance;   
}

singleton.hpp(在栈上创建对象)

#pragma once
 
#include <iostream>
using namespace std;
 
class Singleton{
private:
    Singleton(){
        cout << "创建了一个单例对象" << endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        // 析构函数我们也需要声明成private的
        //因为我们想要这个实例在程序运行的整个过程中都存在
        //所以我们不允许实例自己主动调用析构函数释放对象
        cout << "销毁了一个单例对象" << endl;                                            
    }
         
public:
    static Singleton* getInstance();                                        
};

Singleton* Singleton::getInstance(){
    Lock();    //加锁
    static Singleton instance;
    UnLock();    //解锁

    return &instance;   
}

其中,这些Lock()和UnLock()我并没有实现,这是一个加锁的步骤而已,都是常见的线程同步的方法,所以懂就好啦~~~

当两个线程同时想创建一个实例,由于在一个时刻只有一个线程能得到同步锁,当一个线程加上锁时,第二个线程只能等待,当第一个线程发现实例还没有创建时,它创建出第一个实例。接着第一个线程释放同步锁,此时第二个线程可以加上同步锁,这个时候由于实例已经被第一个线程创建出来了,第二个线程就不会重复创建了,这样就保证了我们在多线程环境中也只能得到一个实例。

但是问题就出来了,加锁和解锁的操作是需要时间的,因此在线程很多的情况下,就会浪费大量的时间,导致效率下降。下面我们可以用一种优化的方法(因为只改动了getInstance这个函数,于是我只把这个函数写了出来)。

Singleton* Singleton::getInstance(){
    if(instance == NULL){
        Lock(); // 加锁
        if(instance == NULL)
            instance = new Singleton();
        UnLock(); // 解锁
    }
    return instance;   
}

上面这种方法只有在instance为空的时候才需要加锁和解锁操作,如果已经创建出来了实例,则无需加锁。所以上面的代码只有在第一个创建实例时才会需要加锁。这样的方法效率已经很好了,只是实现起来比较麻烦,容易出错。

到这里单例模式的懒汉和饿汉模式就都介绍完了,总结一下,懒汉和饿汉分别适用于什么场景。

  • 懒汉模式适用于线程比较少的场景,因为线程一旦多,加锁的开销就会体现出来(当然最后对懒汉优化的那种方案已经差不多解决这个问题了),总之它是一种时间换空间的模式。
  • 饿汉模式适用于线程比较多的场景,它会占用全局静态区一定的空间,但是能够确保只有一个实例。但是当线程很少,甚至是没有用到这个单例类的时候,就显得得不偿失了,它占用的空间问题就体现出来了,这是一种空间换时间的模式。

猜你喜欢

转载自blog.csdn.net/lvyibin890/article/details/81946863