分享

单例-怎么用单例

 小仙女本仙人 2021-07-12

单例的实现

在实现一个单例时,需要从如下角度来考虑单例的实现是否合理:
1.不应该提供创建多个实例的接口。主要是两部分内容,分别是要将构造函数设为private属性,防止在别处通过new创建实例;在创建实例时要考虑线程安全问题,避免在多线程环境中运行时,同一时间多个线程竞争,导致创建了多个实例。
2.考虑是否支持延迟加载。根据系统性能要求的不同,有的系统要求启动速度快,那就不能在启动时创建实例;有的系统要求获取实例时快,那就不能在调用单例时创建实例。
3.考虑Getinstance性能是否高(是否加锁),如果每次获取单例都要加锁解锁,那么对性能的影响是很大的。

饿汉式

懒汉式单例从生命周期来讲,是“与天地同寿的”,在调用main函数之前,单例的构造函数就已经被调用必完成了初始化。(在msvc C++中,真正的函数入口是msvcStartup而不是main函数,在调用main函数之前,
会执行全局对象的初始化,具体实现是这样的:在编译时,将全局构造函数的指针放在一个特殊的段(.ctors段)中,在调用msvcStartup时,遍历该段,将里面所有的构造函数执行一遍)。具体实现如下:

//Signleton.h
class Signleton
{
public:
    virtual ~Signleton();
    Signleton& Getinstance();

private:
    Signleton();//先藏起构造函数
};

//Signleton.cpp
Signleton instance;//这个是放在cpp文件里面的全局变量。

Signleton& Signleton::Getinstance() {
    return instance;//不需要加锁因为在系统构造前就建好了实例
}

饿汉式由于是在系统启动之前就存在了,显然是不支持延迟加载的。与此同时,由于先于系统启动,也不需要
但是,C++中,全局构造函数的调用顺序是未知的,也就是说,如果这个单例中构造时依赖了其他单例,那这个行为就是未定义的。其次,如果单例构造时需要用到系统的一些参数,那这些参数是无法获得的,因为系统都还没启动。。。
那么,饿汉式需不需要考虑构造时线程问题呢?不用的,肯定是在主线程执行的时候由主线程创建的,没别的线程来竞争。
那支持延时加载吗?不支持的,他比谁都先加载出来
那需要加锁吗?不需要的,因为不管多少个线程同时调用GetInstance,Getinstance返回的结果都是一样的,都是系统启动前加载的那个。

懒汉式

懒汉式恰恰和饿汉式相反,生存周期可以说是“应运而生”,就是说只有在第一次调用Getinstance时才会被实例化。具体实现如下:

//Signleton.h
class Signleton
{
public:
    virtual ~Signleton();
    Signleton& Getinstance();

private:
    Signleton();//先藏起构造函数
private:
    Signleton* mInstance;
};

//Signleton.cpp
Signleton* Signleton::mInstance = nullptr;

Signleton& Signleton::Getinstance() {
    if (nullptr == mInstance) {
        mInstance = new Signleton;
    }
    return *mInstance;
}

那么,还是使用你是三个问题来评判懒汉式的实现:
首先是是否支持多线程环境?答案是不支持的,在多线程环境下,如果两个线程th1th2同时执行并且都执行到了 nullptr == mInstance这个判断的时候,th1th2将同时、分别生成两个实例instance1instance2。所以懒汉式不支持多线程环境
其次是判断懒汉式是否支持延迟加载?由于是在第一次调用的时候实例化的,满足延迟加载的条件。

双重检查式

为了满足在多线程环境下运行的需求,现在对懒汉式实现打点补丁。具体实现如下:

//Signleton.h
class Signleton
{
public:
    virtual ~Signleton();
    Signleton& Getinstance();

private:
    Signleton();//先藏起构造函数
private:
    Signleton* mInstance;
    mutex mMutex;
};

//Signleton.cpp
Signleton* Signleton::mInstance = nullptr;

Signleton& Signleton::Getinstance() {
    if (nullptr == mInstance) {//注意这里是判断了两次
        mMutex.lock();
        if (nullptr == mInstance) {
            mInstance = new Signleton;
        }
        mMutex.unlock();
    }
    return *mInstance;
}

打了补丁之后的懒汉式单例,既可以支持延时加载,又可以在多线程环境下运行。在调用
GetInstance的时候,先判断是否已经实例化,如果没有实例化,就加锁,并且实例化。实例化的时候,其他线程调用mMutex.lock()将会被阻塞住。实现在多线程环境下只实例化一个对象的目的。
可是,为什么是两次判断?Getinstance像下面实现不好吗?

Signleton& Signleton::Getinstance() {
        mMutex.lock();
        if (nullptr == mInstance) {
            mInstance = new Signleton;
        }
        mMutex.unlock();
    return *mInstance;
}

这样的话,每次调用Getinstance都会加锁解锁,开销很大,降低了系统效率。
如果要提升系统效率,像下面那样实现行不行?

Signleton& Signleton::Getinstance() {
    if (nullptr == mInstance) {
        mMutex.lock();
        mInstance = new Signleton;
        mMutex.unlock();
    }
    return *mInstance;
}

其实也不行,这样也会存在同时多个线程通过nullptr == mInstance判断的情况,这种写法根本不能支持多线程环境,具体点说就是线程th1,th2同时都通过了外层判断到了内层,此时th1拿到锁,th2阻塞,当th1实例化完成时,th2就会直接再新建一个实例并返回,就是说Signleton被实例化了两次。
所以,两次判断,也是有两个目的的,首先,要在没有实例化时,实例化对象,通多外层判断加快第二次调用Getinstance的性能。其次,内层判断,是要防止当前线程拿到锁的那一刻,别的线程没有实例化对象完成。

局部变量式

从效果上来看,双重加锁仿佛已经是无敌了,及支持延时加载,也可以支持多线程并发,同时加锁也仅仅是在第一次调用,效率较高。还有一种写法,原理上和双重加锁类似,但是更加简洁,具体实现如下:

Signleton& Signleton::Getinstance() {
        static Singleton value;  //静态局部变量
        return value;
    }

上面的式子,利用了局部静态变量在第一次被调用时就会初始化的特点,而且C++11标注明确规定了静态变量的实例化是线程安全的,所以说只有第一次加载且线程安全这两个条件,都利用C++自身的特性实现了,所以我说思想和双重检测类似,但是写法更简单。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多