相信很多小伙伴,对单例模式很熟悉,但是对于选择哪一种单例模式方案,可能不是特别清楚。
对网上五花百门的实现方式,是不是觉得很头大,到底这些方案都有些啥缺点,啥优点,哪种最完美,可以作为自己的常用代码库。
如果有耐心,请仔细阅读下文,带你回顾一下程序员对于单例模式实现方案的辛酸历程。
没有耐心的话o(*^@^*)o,请直接跳到《C++11实现线程安全单例》章节,你将得到一份完美的多线程安全单例模式代码。
1、C++11前,程序员们是怎么实现单例模式?
(1)懒汉模式与饿汉模式
懒汉模式,顾名思义,就是比较懒惰,不是今天必须干的事,坚决放到明天来完成,带有延迟加载的意思。
饿汉模式,意思就是饿了的流浪汉,这种流浪汉什么事情都可以干的,但凡路边的垃圾,街上的妹纸,他都可以吃得下去,饥不择食;
软件一起来,就尽早吃内存,带有提前加载的意思(哪怕暂时用不到)。
我们举一个栗子,比如键盘,一个系统正常输入,我只需要一个键盘,就可以了,所以键盘设计为一个单例,有个打字方法writeWords()。
class Keyboard
{
public:
Keyboard() {}
~Keyboard() {}
static Keyboard* instance()
{
return _pInstance;
}
void writeWords() { }
private:
static Keyboard* _pInstance;
};
Keyboard* Keyboard::_pInstance = new Keyboard(); class Keyboard
{
public:
Keyboard() {}
~Keyboard() {}
static Keyboard* instance()
{
if (!_pInstance)
{
_pInstance = new Keyboard();
}
return _pInstance;
}
void writeWords() { }
private:
static Keyboard* _pInstance;
};
Keyboard* Keyboard::_pInstance = NULL; (2)禁止构造函数、拷贝构造与赋值函数
既然是单例,肯定不允许外面调用构造函数实例化新对象;也不允许拷贝间接实例化新对象;也不允许对象赋值。
class Keyboard
{
private:
Keyboard() = default;
~Keyboard() = default;
Keyboard(const Keyboard&)=delete;
Keyboard& operator=(const Keyboard&)=delete;
public:
static Keyboard* instance()
{
return _pInstance;
}
void writeWords() { }
private:
static Keyboard* _pInstance;
};
Keyboard* Keyboard::_pInstance = new Keyboard(); class Keyboard
{
private:
Keyboard() = default;
~Keyboard() = default;
Keyboard(const Keyboard&)=delete;
Keyboard& operator=(const Keyboard&)=delete;
public:
static Keyboard* instance()
{
if (!_pInstance)
{
_pInstance = new Keyboard();
}
return _pInstance;
}
void writeWords() { }
private:
static Keyboard* _pInstance;
};
Keyboard* Keyboard::_pInstance = NULL; (3)单例的模板化
现在我们再来举个栗子,现在已有一个键盘单例了,像这样的单例,我们还需要很多个,比如鼠标、显示器、耳机。。。(请忽略合理性)。
假设我们选择懒汉模式,那么我们还需要分别添加Mouse、Displayer、Headset三个类,且其实现单例功能的代码和Keyboard高度类似。
发现了吗,小伙伴,我们在做重复性工作了,so,我们需要将单例类模板化,这样每个不同的类需要实现单例,可以套用一个模板。
template <class T>
class Singleton
{
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
public:
static T* instance() { return _instance; }
private:
static T* _instance;
};
template <class T>
T* Singleton<T>::_instance = new T(); template <class T>
class Singleton
{
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
public:
static T* instance()
{
if (!_instance)
{
_instance = new T();
}
return _instance;
}
private:
static T* _instance;
};
template <class T>
T* Singleton<T>::_instance = NULL; 饿汉模式的缺点:也是比较明显,不使用却占用资源,不支持向单例构造函数传参;
优点:就是节省了运行时间(资源提前加载),另外天生自带线程安全属性(在多线程环境下肯定是线程安全的,因为不存在多线程实例化的问题)。
(4)懒汉模式之线程安全性探索
a.懒汉模式下,在定义_instance 变量时先等于NULL,在调用instance()方法时,再判断是否要赋值。这种模式,并非是线程安全的,因为多个线程同时调用instance()方法,就可能导致有产生多个实例。比如A线程执行到第13行之后,第15行之前,当前_instance==NULL,此时由于线程调度,切到B线程,B线程发现_instance==NULL,则进入new T()进行实例化,实例化完成,返回对象指针,然后某一刻发生线程调度,切回到A线程,A线程从以前被打断的地方继续执行,发现_instance==NULL,则进入new T()进行实例化,这样就出现了2个单例对象。显然这是线程非安全的。
那么,要实现线程安全,就必须加锁,以保证对象实例化的原子性。
template <class T>
class Singleton
{
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
public:
static T* instance()
{
std::lock_guard<std::mutex> lock_(m_cs);
if (!_instance)
{
_instance = new T();
}
return _instance;
}
private:
static T* _instance;
static std::mutex m_cs;
};
template <class T>
T* Singleton<T>::_instance = NULL;
template <class T>
std::mutex Singleton<T>::m_cs;但是似乎引出了其他的问题,我们在每次调用instance()时,都会调用进一次加/解锁,但是实际上这个锁只在我们第一次创建对象时,用来防止多线程竞争起到作用。对象创建起来后,多线程都是读取操作,没有写入操作,所以就不会有安全性问题,此后的调用,我们无疑浪费了很多资源。
b.此时我们需要引出一个高大上的名称:DCLP(Double-Checked Locking Pattern),即“双检锁”。怎么操作?就是加个if判断。
template <class T>
class Singleton
{
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
public:
static T* instance()
{
if (!_instance)
{
std::lock_guard<std::mutex> lock_(m_cs);
if (!_instance)
{
_instance = new T();
}
}
return _instance;
}
private:
static T* _instance;
static std::mutex m_cs;
};
template <class T>
T* Singleton<T>::_instance = NULL;
template <class T>
std::mutex Singleton<T>::m_cs;注意到_instance = new T(),是一个写操作,前面有一个无锁的读操作。当真正的写操作进行时,前面的读操作存在脏读情况。
另外其他原因:https://blog.csdn.net/flyingleo1981/article/details/45485293?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1
这对于码农来说,经过一番折腾,然而却没有得到一个完美的解决方案,这是残忍的。。。
那么,有没有线程安全的懒汉模式单例实现方案呢?答案是有,还好有你(C++11)。下一节介绍C++11实现线程安全单例。
2、C++11实现线程安全单例(懒汉)
在C++11中提供一种方法,使得函数可以线程安全的只调用一次。即使用std::call_once和std::once_flag。std::call_once是一种lazy load的很简单易用的机制。
template <class T>
class Singleton
{
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
public:
static T* instance()
{
std::call_once(_flag, [&](){
_instance = new T();
});
return _instance;
}
private:
static T* _instance;
static std::once_flag _flag;
};
template <class T>
T* Singleton<T>::_instance = NULL;
template <class T>
std::once_flag Singleton<T>::_flag;之前我们的单例,instance()只能创建默认构造函数的对象,但是有时候需要给单例传递参数,那么我们需要对instance()方法进行改造,在c++11中,已经支持了可变参数函数。
template <class T>
class Singleton
{
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
public:
template <typename... Args>
static T* instance(Args&&... args)
{
std::call_once(_flag, &](){
_instance = new T(std::forward<Args>(args)...);
});
return _instance;
}
private:
static T* _instance;
static std::once_flag _flag;
};
template <class T>
T* Singleton<T>::_instance = NULL;
template <class T>
std::once_flag Singleton<T>::_flag;一般而言,单例对象无需手动释放,程序结束后,由操作系统自动回收资源。但是为了某些时候特殊处理,我们还是添加上destroy()方法。
template <class T>
class Singleton
{
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
public:
template <typename... Args>
static T* instance(Args&&... args)
{
std::call_once(_flag, [&](){
_instance = new T(std::forward<Args>(args)...);
});
return _instance;
}
static void destroy()
{
if (_instance)
{
delete _instance;
_instance = NULL;
}
}
private:
static T* _instance;
static std::once_flag _flag;
};
template <class T>
T* Singleton<T>::_instance = NULL;
template <class T>
std::once_flag Singleton<T>::_flag;另外还有一个版本的懒汉模式代码,也是支持线程安全(打开编译器C++11支持),大家看看,大概长这样:
class Singleton
{
public:
static Singleton* instance()
{
static Singleton _instance;
return &s_instance;
}
private:
Singleton() {}
};这里使用了局部静态变量,C++11机制可以保证它的初始化具有原子性,线程安全。
这个对象保存在静态数据区,和全局变量是在一起的,而不是在堆中。
3、结论(拿干货)
经过上面的角逐,现在剩下3位选手:饿汉最终版、懒汉版本1、懒汉版本2。
饿汉与懒汉阵营PK:饿汉资源提前加载,浪费比较严重,尤其有一些功能,用户可以选择性启用的,用户如果不需要,
犯不着一上来就占用额外资源;懒汉不存在资源浪费,且同时具备线程安全。
个人觉得还是推荐使用懒汉模式,支持线程安全,构造函数传参,手动回收资源。
套用上面一句话:习惯让我感觉对象保存在堆中更好,至于是不是,待解释。
#include <QCoreApplication>
#include <iostream>
#include "singleton.h"
class Keyboard
{
public:
Keyboard(int a = 0, float b = 0.0)
{
std::cout << "Keyboard():" << (a+b) << std::endl;
}
~Keyboard()
{
std::cout << "~Keyboard()" << std::endl;
}
void writeWords()
{
std::cout << "I'm writing! addr : " << (int)this << std::endl;
}
};
int main(int argc, char *argv[])
{
Keyboard* t1 = Singleton<Keyboard>::instance(5, 2.0);
Keyboard* t2 = Singleton<Keyboard>::instance(6, 5.0);
t1->writeWords();
t2->writeWords();
Singleton<Keyboard>::destroy();
QCoreApplication a(argc, argv);
return a.exec();
}https://download.csdn.net/download/u011832525/12306370
若对你有帮助,欢迎点赞、收藏、评论,你的支持就是我的最大动力!!!
同时,阿超为大家准备了丰富的学习资料,欢迎关注公众号“超哥学编程”,即可领取。
本文涉及工程代码,公众号回复:Singleton,即可下载。

