关于单例模式中懒汉模式和饿汉模式的学习
Hello,各位看官好,本人最近在做项目的过程中遇到了多线程的单例问题,之后查询资料才明白懒汉模式和饿汉模式是咋回事,于是就写了一篇文章来总结这个东西。
一、关于单例模式的简单说明
二、关于懒汉模式的说明
三、关于饿汉模式的说明
四、关于两者的折中方法
五、总结
一、关于单例模式的简单说明
首先我们总结一下单例模式。单例模式其实严格来说也算得上是一种全局变量,它的核心原则就是利用静态成员变量来实现全局只有一个这样的指针。它的重点有以下几处:1、它的指针只能是静态的。2、在写单例的时候需要把拷贝构造函数和运算符重载函数封掉,因为用不到。在书写单例模式的时候有两种写法,第一种叫做懒汉模式,第二种叫做饿汉模式。两者的不同之处在于前者是在用的时候实例化,后者是在一开始的时候实例化(可以把它看成标准的全局变量)
二、关于懒汉模式的总结
首先我们来说一下懒汉模式。懒汉模式的核心部分就在于在执行过程中来进行实例化。我们看一下代码。
.h文件
class singleton
{
public:
singleton();
virtual ~singleton() = default;
singleton(const singleton &rhs) = delete;
singleton operator=(const singleton &) = delete;
public:
static std::shared_ptr<singleton> create();
void init();
void print();
private:
static std::shared_ptr<singleton> instance;
static int count;
};
.cpp文件
std::shared_ptr<singleton> singleton::create()
{
if (instance == nullptr) {
instance = std::make_shared<singleton>();
if (instance != nullptr) {
instance->init();
}
}
return instance;
}
主函数:
singleton::create()
看到了吗?他的核心原则就是在实例化的过程中来进行实例化。如果我们多调几次,那么只会实例化一次。但是他不是线程安全的。我们可以写一个小demo测试一下。
我们在主函数中这样写:
void threadFunc(void *p)
{
DWORD id = GetCurrentThreadId(); // 获得线程id
qDebug()<<"id is:"<<id;
singleton::create()->print(); // 构造函数并获得实例,调用静态成员函数
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
HANDLE m_thread[100];
for (int i=0;i<3;i++){
m_thread[i] = (HANDLE)_beginthread(threadFunc,0,nullptr);
}
for(int i = 0; i<3; i++){
WaitForSingleObject(m_thread[i], INFINITE);
}
w.show();
return a.exec();
}
在cpp中添加
singleton::singleton()
{
count++;
qDebug()<<"start";
Sleep(1000);
qDebug()<<"end";
}
void hungrySinglton::print()
{
qDebug()<<"hcount is:"<<hcount;
}
简单说一下代码,当前我们创建一个子线程,在子线程中进行单例化,如果是线程安全的,那么他会按照顺序打出。比如说:
但是我们实际打出的值为:
以上程序说明我们三个线程都进行实例化了。所以这样线程是不安全的。
三、关于饿汉模式的说明
之前也说了,饿汉模式是一开始就完成了初始化,他的过程可以这么写:
.h文件的写法
class hungrySinglton
{
public:
hungrySinglton();
virtual ~hungrySinglton() = default;
hungrySinglton(const hungrySinglton &rhs) = delete;
hungrySinglton operator=(const hungrySinglton &rhs) = delete;
public:
static std::shared_ptr<hungrySinglton> create();
void print();
public:
static std::shared_ptr<hungrySinglton> instance;
static int hcount;
};
.cpp文件
std::shared_ptr<hungrySinglton> hungrySinglton::instance = std::make_shared<hungrySinglton>(); //这是关键,在这里就处理了
在处理中可以这样写:
std::shared_ptr<hungrySinglton> hungrySinglton::create()
{
return instance;
}
这个好处就是能保证线程安全,但是会浪费内存。他的运行结果为:
我们看这样只运行了一次,线程是安全的。
四、关于两者的折中办法
还有一种方式就是加锁,这样既可以不浪费内存,又可以保证线程安全。
我们来看下面的代码:
if (instance == nullptr) {
std::lock_guard<std::mutex> lk(m_mutex);
if (instance == nullptr) {
instance = std::make_shared<singleton>();
if (instance != nullptr) {
instance->init();
}
}
}
简单解释一下这个代码:
首先第一个判断是因为加锁会很耗费内存,所以加一个判断不用每一次判断了。
加下来加锁,就是保证后续的操作都是原子的。
这样就能保证线程安全了。
五、总结
简单说一下,本文主要介绍了单例模式中懒汉模式和俄汉模式的区别以及写法。需要说一下,在多CPU中,执行顺序有可能改动,这样也有可能会出问题,不过一般情况下这样就可以了。