相信很多小伙伴,对单例模式很熟悉,但是对于选择哪一种单例模式方案,可能不是特别清楚。
对网上五花百门的实现方式,是不是觉得很头大,到底这些方案都有些啥缺点,啥优点,哪种最完美,可以作为自己的常用代码库。
如果有耐心,请仔细阅读下文,带你回顾一下程序员对于单例模式实现方案的辛酸历程。
没有耐心的话o(*^@^*)o,请直接跳到《C++11实现线程安全单例》章节,你将得到一份完美的多线程安全单例模式代码。
后面我会把完整代码贴出来,供下载。
首先来了解一下懒汉模式与饿汉模式。
懒汉模式,顾名思义,就是比较懒惰,不是今天必须干的事,坚决放到明天来完成,带有延迟加载的意思。
饿汉模式,意思就是饿了的流浪汉,这种流浪汉什么事情都可以干的,但凡路边的垃圾,街上的妹纸,他都可以吃得下去,饥不择食;
软件一起来,就尽早吃内存,带有提前加载的意思(哪怕暂时用不到)。
我们举一个栗子,比如键盘,一个系统正常输入,我只需要一个键盘,就可以了,所以键盘设计为一个单例,有个打字方法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;
既然是单例,肯定不允许外面调用构造函数实例化新对象;也不允许拷贝间接实例化新对象;也不允许对象赋值。
我们改造下上面的懒汉与饿汉。
饿汉模式代码:
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;
现在我们再来举个栗子,现在已有一个键盘单例了,像这样的单例,我们还需要很多个,比如鼠标、显示器、耳机。。。(请忽略合理性)。
假设我们选择懒汉模式,那么我们还需要分别添加Mouse、Displayer、Headset三个类,且其实现单例功能的代码和Keyboard高度类似。
发现了吗,小伙伴,我们在做重复性工作了,so,我们需要将单例类模板化,这样每个不同的类需要实现单例,可以套用一个模板。
饿汉模式代码:
template
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
T* Singleton::_instance = new T();
懒汉模式代码:
template
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
T* Singleton::_instance = NULL;
到此,饿汉模式就是最终版本了,后续不会对它进行改造了。
饿汉模式的缺点:也是比较明显,不使用却占用资源,不支持向单例构造函数传参;
优点:就是节省了运行时间(资源提前加载),另外天生自带线程安全属性(在多线程环境下肯定是线程安全的,因为不存在多线程实例化的问题)。
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 Singleton
{
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
public:
static T* instance()
{
std::lock_guard lock_(m_cs);
if (!_instance)
{
_instance = new T();
}
return _instance;
}
private:
static T* _instance;
static std::mutex m_cs;
};
template
T* Singleton::_instance = NULL;
template
std::mutex Singleton::m_cs;
似乎解决了多线程实例化安全性问题,完美?
但是似乎引出了其他的问题,我们在每次调用instance()时,都会调用进一次加/解锁,但是实际上这个锁只在我们第一次创建对象时,用来防止多线程竞争起到作用。对象创建起来后,多线程都是读取操作,没有写入操作,所以就不会有安全性问题,此后的调用,我们无疑浪费了很多资源。
b.此时我们需要引出一个高大上的名称:DCLP(Double-Checked Locking Pattern),即“双检锁”。怎么操作?就是加个if判断。
改造后的代码:
template
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 lock_(m_cs);
if (!_instance)
{
_instance = new T();
}
}
return _instance;
}
private:
static T* _instance;
static std::mutex m_cs;
};
template
T* Singleton::_instance = NULL;
template
std::mutex Singleton::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实现线程安全单例。
在C++11中提供一种方法,使得函数可以线程安全的只调用一次。即使用std::call_once和std::once_flag。std::call_once是一种lazy load的很简单易用的机制。
懒汉模式改造后的代码:
template
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
T* Singleton::_instance = NULL;
template
std::once_flag Singleton::_flag;
之前我们的单例,instance()只能创建默认构造函数的对象,但是有时候需要给单例传递参数,那么我们需要对instance()方法进行改造,在c++11中,已经支持了可变参数函数。
然而向单例构造函数中传参,这个需求,饿汉模式就无法实现了。
下面我们继续改造,添加构造函数传参。
懒汉模式改造后的代码:
template
class Singleton
{
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
public:
template
static T* instance(Args&&... args)
{
std::call_once(_flag, &](){
_instance = new T(std::forward(args)...);
});
return _instance;
}
private:
static T* _instance;
static std::once_flag _flag;
};
template
T* Singleton::_instance = NULL;
template
std::once_flag Singleton::_flag;
一般而言,单例对象无需手动释放,程序结束后,由操作系统自动回收资源。但是为了某些时候特殊处理,我们还是添加上destroy()方法。
改造后的代码:
template
class Singleton
{
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
public:
template
static T* instance(Args&&... args)
{
std::call_once(_flag, [&](){
_instance = new T(std::forward(args)...);
});
return _instance;
}
static void destroy()
{
if (_instance)
{
delete _instance;
_instance = NULL;
}
}
private:
static T* _instance;
static std::once_flag _flag;
};
template
T* Singleton::_instance = NULL;
template
std::once_flag Singleton::_flag;
到此,这里为懒汉模式的最终版,我们姑且称之为懒汉版本1。
在C++11标准中,要求局部静态变量初始化具有线程安全性。
另外还有一个版本的懒汉模式代码,也是支持线程安全(打开编译器C++11支持),大家看看,大概长这样:
class Singleton
{
public:
static Singleton* instance()
{
static Singleton _instance;
return &s_instance;
}
private:
Singleton() {}
};
这里使用了局部静态变量,C++11机制可以保证它的初始化具有原子性,线程安全。
这个对象保存在静态数据区,和全局变量是在一起的,而不是在堆中。
习惯让我感觉对象保存在堆中更好,至于是不是,待解释。
姑且称这个为懒汉版本2。
经过上面的角逐,现在剩下3位选手:饿汉最终版、懒汉版本1、懒汉版本2。
饿汉与懒汉阵营PK:饿汉资源提前加载,浪费比较严重,尤其有一些功能,用户可以选择性启用的,用户如果不需要,
犯不着一上来就占用额外资源;懒汉不存在资源浪费,且同时具备线程安全。
第一局:懒汉胜利,选择懒汉。
个人觉得还是推荐使用懒汉模式,支持线程安全,构造函数传参,手动回收资源。
第二局:懒汉版本1 PK 版本2。
套用上面一句话:习惯让我感觉对象保存在堆中更好,至于是不是,待解释。
所以我们选择懒汉版本1。真是玩笑了,O(∩_∩)O哈哈~
具体用哪个懒汉,看个人喜好吧。
我个人比较倾向于懒汉版本1。
下面是测试代码(main.cpp):
#include
#include
#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::instance(5, 2.0);
Keyboard* t2 = Singleton::instance(6, 5.0);
t1->writeWords();
t2->writeWords();
Singleton::destroy();
QCoreApplication a(argc, argv);
return a.exec();
}
结果:
C++11实现线程安全单例代码和测试代码,下载地址:
https://download.csdn.net/download/u011832525/12306370
代码git地址:
https://gitee.com/bailiyang/cdemo/tree/master/DesignPattern/Singleton
===================================================
===================================================
他来了,他来了,他带着礼物走来了