单例模式笔记

Singleton

单例模式(Singleton Pattern)是最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要显式实例化该类的对象。

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。

注意:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

优点

  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
  • 避免对资源的多重占用(比如写文件操作)。

缺点

没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景

1、要求生产唯一序列号。

2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。

3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

4、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。

1. Lazy Singleton

这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁,所以严格意义上它并不算单例模式,适用于不要求线程安全的场景。

#ifndef LAZY_SINGLETON_H
#define LAZY_SINGLETON_H

class LazySingleton
{
private:
    static LazySingleton *instance;

private:
    LazySingleton();
    ~LazySingleton();
    LazySingleton(const LazySingleton &);
    LazySingleton &operator=(const LazySingleton &);

public:
    static LazySingleton *getInstance();
};

#endif
#include "LazySingleton.h"
#include <iostream>
LazySingleton *LazySingleton::instance = nullptr;
LazySingleton *LazySingleton::getInstance()
{
    if (instance == nullptr)
    {
        instance = new LazySingleton();
    }
    return instance;
}
LazySingleton::LazySingleton()
{
    std::cout << "LazySingleton initialized!" << std::endl;
}
LazySingleton::~LazySingleton()
{
    std::cout << "LazySingleton destroyed!" << std::endl;
}

上述Lazy Singleton的实现存在内存泄露的问题,有两种解决方法:

  • 使用智能指针
  • 使用静态的嵌套类对象

1.1 使用智能指针解决内存泄漏

#ifndef SHARED_SINGLETON_H
#define SHARED_SINGLETON_H
#include <memory>
class SharedSingleton
{
private:
    static std::shared_ptr<SharedSingleton> instance;

    SharedSingleton();
    ~SharedSingleton();
    SharedSingleton(const SharedSingleton &);
    SharedSingleton &operator=(const SharedSingleton &);

public:
    static std::shared_ptr<SharedSingleton> getInstance();
};

#endif
#include "SharedSingleton.h"
#include <iostream>
#include <memory>

std::shared_ptr<SharedSingleton> SharedSingleton::instance = nullptr;
std::shared_ptr<SharedSingleton> SharedSingleton::getInstance()
{
    if (instance == nullptr)
    {
        instance = std::shared_ptr<SharedSingleton>(
          new SharedSingleton, [](SharedSingleton *obj){
            delete obj; 
          });
    }
    return instance;
}
SharedSingleton::SharedSingleton()
{
    std::cout << "SharedSingleton initialized!" << std::endl;
}
SharedSingleton::~SharedSingleton()
{
    std::cout << "SharedSingleton destroyed!" << std::endl;
}

1.2 使用静态嵌套类对象解决内存泄漏

#ifndef INNER_SINGLETON_H
#define INNER_SINGLETON_H

class InnerDeletorSingleton
{
private:
    static InnerDeletorSingleton *instance;

private:
    InnerDeletorSingleton();
    ~InnerDeletorSingleton();
    InnerDeletorSingleton(const InnerDeletorSingleton &);
    InnerDeletorSingleton &operator=(const InnerDeletorSingleton &);

private:
    class Deletor
    {
    public:
        ~Deletor()
        {
            if (InnerDeletorSingleton::instance != nullptr)
                delete InnerDeletorSingleton::instance;
        }
    };
    static Deletor deletor;

public:
    static InnerDeletorSingleton *getInstance();
};
#endif
#include "InnerDeletorSingleton.h"
#include <iostream>
InnerDeletorSingleton::Deletor InnerDeletorSingleton::deletor;
InnerDeletorSingleton *InnerDeletorSingleton::instance = nullptr;
InnerDeletorSingleton *InnerDeletorSingleton::getInstance()
{
    if (instance == nullptr)
    {
        instance = new InnerDeletorSingleton();
    }
    return instance;
}

InnerDeletorSingleton::InnerDeletorSingleton()
{
    std::cout << "InnerDeletorSingleton initialized!" << std::endl;
}
InnerDeletorSingleton::~InnerDeletorSingleton()
{
    std::cout << "InnerDeletorSingleton destroyed!" << std::endl;
}

1.3 线程安全的Singleton方案

Lazy Singleton模式的竞争条件主要出现在第一次初始化的过程中,instance = new Singleton()处,即可能多个线程同时检测到instance未被初始化,于是开始执行初始化工作,为了避免重复初始化,需要对这一过程上锁。

#ifndef DCL_SINGLETON_H
#define DCL_SINGLETON_H
#include <thread>
class DCLSingleton
{
private:
    static DCLSingleton *instance;
    DCLSingleton();
    ~DCLSingleton();
    DCLSingleton(const DCLSingleton &);
    DCLSingleton &operator=(const DCLSingleton &);
    static std::mutex mutex_;

public:
    static DCLSingleton *getInstance();
};

#endif
#include "DCLSingleton.h"
#include <iostream>
DCLSingleton *DCLSingleton::instance = nullptr;
std::mutex DCLSingleton::mutex_;
DCLSingleton *DCLSingleton::getInstance()
{
    if (instance == nullptr)
    {
        std::lock_guard<std::mutex> lk(mutex_);
        if (instance == nullptr)
        {
            instance = new DCLSingleton();
        }
    }
    return instance;
}
DCLSingleton::DCLSingleton()
{
    std::cout << "DCLSingleton initialized!" << std::endl;
}
DCLSingleton::~DCLSingleton()
{
    std::cout << "DCLSingleton destroyed!" << std::endl;
}

加入DCL后,其实还是有问题的,关于memory model。

在某些内存模型中(虽然不常见)或者是由于编译器的优化以及运行时优化等等原因,使得instance虽然已经不是nullptr但是其所指对象还没有完成构造,这种情况下,另一个线程如果调用getInstance()就有可能使用到一个不完全初始化的对象。

在C++11没有出来的时候,只能靠插入两个memory barrier(内存屏障)来解决这个错误,但是C++11引进了memory model,提供了Atomic实现内存的同步访问,即不同线程总是获取对象修改前或修改后的值,无法在对象修改期间获得该对象。

C++11后就可以正确的跨平台的实现DCL模式

std::atomic<DCLSingleton *>DCLSingleton::instance = nullptr;

2. Eager Singleton

#ifndef EAGER_SINGLETON_H
#define EAGER_SINGLETON_H
class EagerSingleton
{
private:
    static EagerSingleton instance;

    EagerSingleton();
    ~EagerSingleton();
    EagerSingleton(const EagerSingleton &);
    EagerSingleton &operator=(const EagerSingleton &);

public:
    static EagerSingleton &getInstance();
};
#endif

#include <iostream>
#include "EagerSingleton.h"
EagerSingleton EagerSingleton::instance;

EagerSingleton &EagerSingleton::getInstance()
{
    return instance;
}

EagerSingleton::EagerSingleton()
{
    std::cout << "EagerSingleton initialized!" << std::endl;
}
EagerSingleton::~EagerSingleton()
{
    std::cout << "EagerSingleton destroyed!" << std::endl;
}

由于在main函数之前初始化,所以没有线程安全的问题。

但是潜在问题在于no-local static对象(函数外的static对象)在不同编译单元中的初始化顺序是未定义的。

也即,static Singleton instance;和static Singleton& getInstance()二者的初始化顺序不确定,如果在初始化完成之前调用 getInstance() 方法会返回一个未定义的实例。

3. Meyers Singleton

C++11规定了local static在多线程条件下的初始化行为,要求编译器保证了内部静态变量的线程安全性

#ifndef MEYER_SINGLETON_H
#define MEYER_SINGLETON_H
class MeyerSingleton
{
private:
	MeyerSingleton();
	~MeyerSingleton();
	MeyerSingleton(const MeyerSingleton &);
	MeyerSingleton &operator=(const MeyerSingleton &);

public:
	static MeyerSingleton &getInstance()
	{
		static MeyerSingleton instance;
		return instance;
	}
};
#endif
#include <iostream>
#include "MeyerSingleton.h"

MeyerSingleton::MeyerSingleton()
{
    std::cout << "MeyerSingleton initialized!" << std::endl;
}
MeyerSingleton::~MeyerSingleton()
{
    std::cout << "MeyerSingleton destroyed!" << std::endl;
}

4 总结

  • Eager Singleton 虽然是线程安全的,但存在潜在问题;
  • Lazy Singleton通常需要加锁来保证线程安全,但局部静态变量版本在C++11后是线程安全的;
  • 局部静态变量版本(Meyers Singleton)最优雅。

5 注意事项

现有ABC三个库,其中A中封装了一个MeyerSingleton单例类,共享库B和共享库C使用A,D作为可执行程序,使用B和C,那么这个单例是否唯一?

以下是摘自chatgpt的回答:

如果A是一个共享库,封装了一个单例类,并且B和C都使用了A,那么D作为可执行程序,使用了B和C,这个单例类在整个程序中仍然是唯一的。单例类的唯一性是相对于进程而言的,因此由A封装的单例类在整个程序执行期间只会有一个实例,即使它被不同的共享库使用。

如果A作为静态库,封装了一个单例类,B和C使用A,D作为可执行程序使用B和C。由于静态库在链接时会被整合到可执行程序中,每个使用A的库和可执行程序中都将包含单例类的一个实例。因此,这个单例类在程序中仍然是唯一的,但是这个唯一性是相对于每个包含A的模块而言的,而不是整个程序。每个模块(B、C和D)都会有自己的单例实例。

此外,当libA作为静态库且在cpp文件中实现getInstance时,libB和libC同时作为动态库时出现单例不一致的问题。当libA作为动态库时,libB和libC作为动态库或者静态库时都没有这个问题 所以为了避免这个问题,最好的方式是,将getInstance的实现内联在.h文件中

posted @ 2024-01-27 16:05  料峭春风吹酒醒  阅读(6)  评论(0编辑  收藏  举报