单例模式
单例模式在日常开发的时候用的还是比较多的,写法呢也不较多,但是了很多时候可能写的不够完善,比如内存没有进行正确释放,没有进行加锁操作。前者可能出现段错误和内存泄漏,后者可能出现多线程竞争的问题。
不多BB直接上代码
class singleton {
public:
static singleton* GetInstance() {
if (_instance == nullptr)
{
_instance = new singleton();
}
return _instance;
}
singleton(const singleton&) = delete;
singleton& operator = (const singleton&) = delete;
private:
singleton() {
}
~singleton() {
if (_instance != nullptr)
delete _instance;
_instance = nullptr;
}
static singleton* _instance;
};
singleton* singleton::_instance = nullptr; //类外进行初始化
很多人写出来的可能就是上面这样子的,有点基础的人肯定就知道,多线程的时候会出现竞争的问题,可能new的时候会new多份,导致内存泄漏,重复加载的问题。
其实问题不止这些 ,因为在创建对象的时候对象在堆上,你拿到的只是一个它的指针,指针在退栈的时候时候不会自动释放的,你去delete这个指针也不行,因为析构函数是私有的。只能说在程序退出的时候由系统给你释放,而操作系统怎么释放我们是不知道的,就有可能导致崩溃。
class singleton {
public:
static singleton* GetInstance() {
if (_instance == nullptr)
{
_instance = new singleton();
atexit(Destructor); /*由于对象在堆上, 栈上只有一个指针,指针在退栈的时候不会释放内存*/
}
return _instance;
}
singleton(const singleton&) = delete;
singleton& operator = (const singleton&) = delete;
private:
static void Destructor() {
if (_instance != nullptr)
delete _instance;
_instance = nullptr;
}
singleton() {
}
~singleton() {
}
static singleton* _instance;
};
singleton* singleton::_instance = nullptr; //类外进行初始化
上面加了一个Destuctor函数,并且用atexit()将其绑定进程退出时调用,上述方式解决了内存释放的问题。
释放内存的方式也不止上面的这么一种,还有内部类,智能指针等等。但是多线程竞争的问题并没有解决。
其实上面这个玩意也能用,笔者在之前的一个项目中,就用的这种方式,基本上没出现啥问题。用我师傅的话说: 一下就过去了,不用管那么多。
下面是加锁版本
class singleton {
public:
static singleton* GetInstance() {
// std::lock_guard<std::mutex> lock1(_mutex1); //第一处
if (_instance == nullptr)
{
// std::lock_guard<std::mutex> lock2(_mutex2); //第二处
if (_instance == nullptr)
{
_instance = new singleton();
}
}
return _instance;
}
singleton(const singleton&) = delete;
singleton& operator = (const singleton&) = delete;
private:
singleton() {
std::cout << "构造函数被调用" << endl;
}
~singleton() {
}
static singleton* _instance;
static std::mutex _mutex1;
static std::mutex _mutex2;
};
singleton* singleton::_instance = nullptr; //类外进行初始化
std::mutex singleton::_mutex1; //类外初始化
std::mutex singleton::_mutex2;
上面的内容一共有两个位置可以加锁,如果在第一处加锁,那么每次取资源都需要加锁,挂起等待的线程会进行线程的切换,资源消耗很大。
第二处加锁 :放开的第二处的代码,双检测的目的是为了提高效率。但是把还是可能存在问题,在执行new的时候,一共三个动作,分配内存,调用构造函数,赋值。由于现在cpu都是流水线模式一次性取多个指令,可能进行指令重排。比如说先进行了开内存和赋值,但是没有调用构造函数,这时候另一个线程看_instance已经有值了,就直接返回了,而这段空间还没有被构造函数初始化,那么就有可能出现问题。出现这个问题的概率我感觉很小很小很小。 比较CPU的频率在哪里摆着,不过肯定还有更优雅的方法。
C++11 中出现了一个特性,static变量在初始化的时候,并发同时进入声明语句,并发线程将会组设等待初始化结束。
class singleton1 {
static singleton1& GetInstance()
{
static singleton1 instance;
return instance;
}
private:
singleton1(const singleton1&);
singleton1(){}
singleton1& operator = (const singleton& ){}
};
这样也是一个比较不错的方法