十三、day13

今天学习如何单例模式实现逻辑层的设计。内容包括服务器如何能捕获信号使其安全退出、单例模标类

1. 什么是单例模式?

单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点,单例模式是在内存中仅会创建一次对象的设计模式。

//通过静态成员变量实现单例
//懒汉式
class Single2
{
private:
    Single2() {}
    Single2(const Single2 &) = delete;
    Single2& operator=(const Single2&) = delete;
public:
    static Single2& GetInst()
    {
        static Single2 single;
        return single;
    }
};
  • 在上面的代码块中,定义了一个Single2类,Single2类的默认构造函数被声明为私有,且删除拷贝构造函数和赋值运算符,确保Single2类不能通过拷贝创建或赋值创建新的实例。

  • Single2类只有一个公共静态方法GetInst(),用于获取Single2类的唯一实例。

  • 局部静态成员single用于存储Single2类的唯一实例,通过返回single即可返回该实例。

单例模式的简单实现可总结为:

  • 构造方法是私有的

  • 对外暴露的获取访问是公有的静态的

  • 唯一实例的存储方式是静态的

风险:上述代码块(懒汉式)生成了唯一实例,但在多线程方式下生成的实例可能会存在多个(如果多个线程同时调用GetInst()时都会去实例化一个simgle对象,使得Single2类被重复实例化)

1)单例模式的分类

  • 饿汉式:类加载就会导致该单实例对象被创建

  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时被创建

懒汉式创建对象的方法是函数中创建静态局部变量,这样只有在对象第一次被使用时才会创建实例;而饿汉式一般已经在类中提前声明了静态变量single,这样在类加载时便已经提前创建好实例。

上述代码块的单例模式就是通过懒汉式实现的,静态变量single在第一次使用Single2类的GetInst()时被创建,其声明周期随着进程结束而结束。

饿汉式单例模式实现

//饿汉式
class Single2Hungry
{
private:
    Single2Hungry() { }
    Single2Hungry(const Single2Hungry&) = delete;
    Single2Hungry& operator=(const Single2Hungry&) = delete;
public:
    static Single2Hungry* GetInst()
    {
        if (single == nullptr)
            single = new Single2Hungry();

        return single;
    }
private:
    static Single2Hungry* single;
};

饿汉模式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,可以避免线程安全问题。

多线程和单线程下进行测试

//饿汉式初始化
Single2Hungry* Single2Hungry::single = Single2Hungry::GetInst();
void thread_func_s2(int i)
{
    cout << "this is thread " << i << endl;
    cout << "inst is " << Single2Hungry::GetInst() << endl;
}
void test_single2hungry()
{
    cout << "s1 addr is " << Single2Hungry::GetInst() << endl;
    cout << "s2 addr is " << Single2Hungry::GetInst() << endl;
    for (int i = 0; i < 3; i++)
    {
        thread tid(thread_func_s2, i);
        tid.join();
    }
}
int main(){
    test_single2hungry()
}

输出为

s1 addr is 0x1e4b00
s2 addr is 0x1e4b00
this is thread 0
inst is 0x1e4b00
this is thread 1
inst is 0x1e4b00
this is thread 2
inst is 0x1e4b00

可见无论单线程还是多线程模式下,通过静态成员变量的指针实现的单例类都是唯一的。饿汉式是在程序启动时就进行单例的初始化,这种方式也可以通过懒汉式调用,无论饿汉式还是懒汉式都存在一个问题,就是什么时候释放内存?多线程情况下,释放内存就很难了,还有二次释放内存的风险。

2)懒汉式的改进

上面提到了懒汉式有一定的风险:在多线程下可能会创建多个Single2的实例,如果多个线程同时调用GetInst()时都会去实例化一个simgle对象,使得Single2类被重复实例化)。

通过对GetInst()方法枷锁或者对Single2类进行加锁,可以解决该风险,每个线程在进入方法前,都要等到别的线程都离开此方法,不会有两个线程同时进入此方法。

//懒汉式指针
//即使创建指针类型也存在问题
class SinglePointer
{
private:
    SinglePointer() { }
    SinglePointer(const SinglePointer&) = delete;
    SinglePointer& operator=(const SinglePointer&) = delete;
public:
    static SinglePointer *GetInst()
    {
        if (single != nullptr)
        {
            return single;
        }
        s_mutex.lock();
        if (single != nullptr)
        {
            s_mutex.unlock();
            return single;
        }
        single = new SinglePointer();
        s_mutex.unlock();
        return single;
    }
private:
    static SinglePointer *single;
    static mutex s_mutex;
};

该段代码块通过双重检验枷锁进行加锁,避免了直接加锁造成的问题:每次去获取对象都需要先获取锁,并发性能非常地差。

双重检验枷锁:

  • 如果已经实例化了,则不需要加锁,直接返回实例化对象

  • 如果没有实例化对象则加锁,然后再判断一次有没有实例化

  • 如果实例化了就解锁并返回实例化对象

  • 如果没有实例化就初始化实例化对象,并解锁返回实例化对象

进行测试

//懒汉式
//在类的cpp文件定义static变量
SinglePointer *SinglePointer::single = nullptr;
std::mutex SinglePointer::s_mutex;

void thread_func_lazy(int i)
{
    cout << "this is lazy thread " << i << endl;
    cout << "inst is " << SinglePointer::GetInst() << endl;
}
void test_singlelazy()
{
    for (int i = 0; i < 3; i++)
    {
        thread tid(thread_func_lazy, i);
        tid.join();
    }
    //何时释放new的对象?造成内存泄漏
}
int main(){
    test_singlelazy();
}

输出为

this is lazy thread 0
inst is 0xbc1700
this is lazy thread 1
inst is 0xbc1700
this is lazy thread 2
inst is 0xbc1700

尽管多线程下懒汉式可能会创建多个Single2类实例的问题被解决,但无论懒汉式还是饿汉式,都有一个共同的问题需要解决:什么时候释放内存?多线程下多次delete也会造成崩溃。

3)智能指针方法

使用智能指针方法自动回收内存的机制设计单例类

//利用智能指针解决释放问题
class SingleAuto
{
private:
    SingleAuto() { }
    SingleAuto(const SingleAuto&) = delete;
    SingleAuto& operator=(const SingleAuto&) = delete;
public:
    ~SingleAuto()
    {
        cout << "single auto delete success " << endl;
    }
    static std::shared_ptr<SingleAuto> GetInst() {
        if (single != nullptr) {
            return single;
        }

        s_mutex.lock();
        if (single != nullptr) {
            s_mutex.unlock();
            return single;
        }

        single = std::make_shared<SingleAuto>();
        s_mutex.unlock();
        return single;
    }
private:
    static std::shared_ptr<SingleAuto> single;
    static mutex s_mutex;
};

SingleAuto类的GetInst()返回std::shared_ptr<SingleAuto>类型的变量single。因为single是静态成员变量,所以会在进程结束时被回收。智能指针被回收时会调用内置指针类型的析构函数,从而完成内存的回收。

测试

// 智能指针方式
std::shared_ptr<SingleAuto> SingleAuto::single = nullptr;
mutex SingleAuto::s_mutex;
void test_singleauto()
{
    auto sp1 = SingleAuto::GetInst();
    auto sp2 = SingleAuto::GetInst();
    cout << "sp1  is  " << sp1 << endl;
    cout << "sp2  is  " << sp2 << endl;
    //此时存在隐患,可以手动删除裸指针,造成崩溃
    // delete sp1.~SingleAuto();
}
int main(){
    test_singleauto();
}

输出:

sp1  is  0x1174f30
sp2  is  0x1174f30

智能指针方式不存在内存泄漏,但是有一个隐患:单例类的析构函数是公有成员,如果被人手动调用会存在崩溃问题,比如将上边测试中的注释打开,程序会崩溃

4)辅助类智能指针单例模式

将析构函数私有化,在构造智能指针时指定删除器,通过传递一个辅助类或者辅助函数帮助智能指针回收内存时调用指定的析构函数。因为析构函数私有化以后,智能指针在引用计数归零后无法调用对象的析构函数进行销毁。所以必须指定一个删除器,该删除器是单例类的友元类,可以访问单例类的私有或公有成员,可以通过删除器间接调用单例类的析构函数。

// safe deletor
//该类定义仿函数调用SingleAutoSafe析构函数
class SingleAutoSafe;

class SafeDeletor
{
public:
    void operator()(SingleAutoSafe *sf)
    {
        cout << "this is safe deleter operator()" << endl;
        delete sf;
    }
};

class SingleAutoSafe
{
private:
    SingleAutoSafe() {}
    ~SingleAutoSafe()
    {
        cout << "this is single auto safe deletor" << endl;
    }
    SingleAutoSafe(const SingleAutoSafe &) = delete;
    SingleAutoSafe &operator=(const SingleAutoSafe &) = delete;
    //定义友元类,通过友元类调用该类析构函数
    friend class SafeDeletor;
public:
    static std::shared_ptr<SingleAutoSafe> GetInst()
    {
        if (single != nullptr)
        {
            return single;
        }
        s_mutex.lock();
        if (single != nullptr)
        {
            s_mutex.unlock();
            return single;
        }
        //额外指定删除器
        single = std::shared_ptr<SingleAutoSafe>(new SingleAutoSafe, SafeDeletor());
        //也可以指定删除函数
        // single = std::shared_ptr<SingleAutoSafe>(new SingleAutoSafe, SafeDelFunc);
        s_mutex.unlock();
        return single;
    }
private:
    static std::shared_ptr<SingleAutoSafe> single;
    static mutex s_mutex;
};

如果是提前声明SafeDeletor ,而先定义SingleAutoSafe,会造成 incomplete type(类型不完整)的错误,因为被提前声明的类,可以在后面定义,但在声明类前面定义的其他类中使用声明类时,只能使用声明类的指针,而不能创建声明类的实例。

SafeDeletor类中重载了(),实现类模拟函数的作用。

SafeDeletor要写在SingleAutoSafe上边,并且SafeDeletor要声明为SingleAutoSafe类的友元类,这样就可以访问SingleAutoSafe的析构函数了。在构造single时制定了SafeDeletor(),single在回收时,会调用仿函数SafeDeletor(),从而完成内存的销毁。同时,SingleAutoSafe的析构函数为私有,无法被外界显式调用。

5)通用的单例模板类

通过声明单例的模板类,然后继承这个单例模板类的所有类就是单例类了,可以达到泛型编程提高效率的目的

template <typename T>
class Single_T
{
protected:
    Single_T() = default;
    Single_T(const Single_T<T> &st) = delete;
    Single_T &operator=(const Single_T<T> &st) = delete;
    ~Single_T()
    {
        cout << "this is auto safe template destruct" << endl;
    }
public:
    static std::shared_ptr<T> GetInst()
    {
        if (single != nullptr)
        {
            return single;
        }
        s_mutex.lock();
        if (single != nullptr)
        {
            s_mutex.unlock();
            return single;
        }
        //额外指定删除器
        single = std::shared_ptr<T>(new T, SafeDeletor_T<T>());
        //也可以指定删除函数
        // single = std::shared_ptr<SingleAutoSafe>(new SingleAutoSafe, SafeDelFunc);
        s_mutex.unlock();
        return single;
    }
private:
    static std::shared_ptr<T> single;
    static mutex s_mutex;
};

//模板类的static成员要放在h文件里初始化
template <typename T>
std::shared_ptr<T> Single_T<T>::single = nullptr;
template <typename T>
mutex Single_T<T>::s_mutex;

模板类的静态成员变量要在头文件中初始化,而非模板类的静态成员变量一般在cpp文件中初始化。

应用:定义一个网络的单例类,继承上述模板类,并将构造和析构设置为私有,同时设置友元保证自己的析构和构造可以被友元类调用.

class SingleNet : public Single_T<SingleNet>
{
private:
    SingleNet() = default;
    SingleNet(const SingleNet &) = delete;
    SingleNet &operator=(const SingleNet &) = delete;
    ~SingleNet() = default;
    friend class SafeDeletor_T<SingleNet>;
    friend class Single_T<SingleNet>;
};

删除器SafeDeletor_T和单例模板类Single_T都是模板,需要提前定义。

测试

void test_singlenet()
{
    auto sp1 = SingleNet::GetInst();
    auto sp2 = SingleNet::GetInst();
    cout << "sp1  is  " << sp1 << endl;
    cout << "sp2  is  " << sp2 << endl;
}

5)总结

1. 为什么要有单例模式?

使用单例模式的原因

  • 资源控制:单例模式可以用来控制系统中的资源,例如数据库连接池或线程池,确保这些关键资源不会被过度使用。

  • 内存节省:当需要一个对象进行全局访问,但创建多个实例会造成资源浪费时,单例模式可以确保只创建一个实例,节省内存。

  • 共享:单例模式允许状态或配置信息在系统的不同部分之间共享,而不需要传递实例。

  • 延迟初始化:单例模式支持延迟初始化,即实例在首次使用时才创建,而不是在类加载时。

  • 一致的接口:单例模式为客户端提供了一个统一的接口来获取类的实例,使得客户端代码更简洁。

  • 易于维护:单例模式使得代码更易于维护,因为所有的实例都使用相同的实例,便于跟踪和修改变更。

单例模式的应用场景

  • 配置管理器:在应用程序中,配置信息通常只需要读取一次,并全局使用。单例模式用于确保配置管理器只被实例化一次。

  • 日志记录器:一个系统中通常只需要一个日志记录器来记录所有的日志信息,使用单例模式可以避免日志文件的重复写入。

  • 数据库连接池:数据库连接是一种有限的资源,使用单例模式可以确保数据库连接池的唯一性,并且能够重用连接,减少连接创建和销毁的开销。

  • 线程池:类似于数据库连接池,线程池也是有限的资源,使用单例模式可以避免创建过多的线程,提高应用程序的并发性能。

  • 任务调度器:在需要全局调度和管理的场景下,如定时任务调度器,单例模式提供了一个集中的管理方式。

  • 网站的计数器:一般也是采用单例模式实现,否则难以同步。

2. 单例模式中GetInst()为什么是静态的?

1)静态方法可以通过类名直接访问,无需创建类的实例。这样可以方便地获取唯一实例,而不需要先实例化类。 2)类的构造函数已经被私有化,无法直接实例化对象,智能通过定义静态成员函数的方式通过类名::方法名的方式进行构造访问 3)静态局部变量的方式是线程安全的,能确保在多线程环境下也只会创建一个实例

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部