学习完C++的静态成员的相关知识,我们先来了解设计模式中最简单的一种:单例模式。
单例模式的动机
对于一个软件系统的某些类而言,我们没有必要创建多个实例化对象,就比如Windows系统的任务管理器或回收站,我们无论点击多少次都只会弹出一个窗口,因为如果弹出多个窗口,其实就是重复对象,势必会浪费系统资源,因此我们需要确保系统中某个类只有唯一一个实例,当这个实例创建成功以后,我们无法再创建同类型的其他对象,所有的操作都只能基于这个唯一的实例,为例确保对象的唯一性,我们提出了单例模式。
一个类不管创建多少次对象,永远只能得到该类型的一个对象示例。
很多时候,我们在程序设计的时候,对于某一类只需要生成其中一个对象。比如说对于MySQL数据库类来说,如果频繁的创建MySQL对象,那么会有海量的链接连接到MySQL数据库,对数据库来说是一个不小的负担。
但其实对于开发者来说,一个连接其实就能胜任与数据库的交互工作,这就是单例模式。
从具体实现的角度来说:
- 限制对象的构建个数:
构造函数私有化
; - 用户无法自定义对象,因此我们需要提供一个:
类定义中含有一个该类的静态私有对象
; - 该类提供了一个静态的共有函数用于创建或者获取它本身的静态私有对象。
1、饿汉单例模式
饿汉模式的实例在主函数运行之前就已经产生(这是静态数据成员的初始化时在主函数运行前就完成造成的),因为它饿了,很着急。^ _ ^…
class Singleton
{
public:
//给用户提供一个接口获取对象,定义成静态,否则普通成员方法的调用还得依赖对象,现在没有对象,是在获取对象
static Singleton & GetInstance()//这里若不以引用返回就会调动拷贝构造作为过渡
{ //但我们的拷贝构造已经删除,无法完成。另一种方案就是以指针返回
return instance;
}
private:
static Singleton instance;//2、定义一个唯一的类的实例对象
Singleton(){}//1、构造函数私有化
//限制对象的生成方式
Singleton(const Singleton & obj) = delete;//防止拷贝构造构建对象
Singleton& operator=(const Singleton & obj) = delete;//防止通过赋值语句构建对象
};
//静态成员初始化必须在类外初始化
Singleton Singleton::instance;
int main()
{
Singleton & obja = Singleton::GetInstance();
Singleton & objb = obja.GetInstance();
cout<<&obja<<endl;
cout<<&objb<<endl;
}
我们可以看到,obja和objb都代表了同一个对象:
注意
:以上的方式创建的对象:一定是线程安全
的,因为多线程执行先执行函数,静态成员变量在数据段,在函数创建前都已经初始化好了。
比如:将如下两个函数放入到两个线程当中
void funa()
{
Singleton & obja = Singleton::GetInstance();
cout<<&obja<<endl;
}
void funb()
{
Singleton & objb = Singleton::GetInstance();
cout<<&objb<<endl;
}
#include<thread>
int main()
{
thread thra(funa);
thread thrb(funb);
thra.join();
thrb.join();
return 0;
}
无论几个线程调用,在构建对象时我们都得到了唯一的实例化对象
总结:
体会:为什么要将这种设计模式定义为饿汉单例模式呢?
即:像一个饿汉一样,不管需不需要用到实例都要去创建实例,即在类产生的时候就创建好实例,这是一种空间换时间的做法。作为一个饿汉而言,体现了它的本质——“我全都要”。
优点
:程序加载时就实例化,之后的操作效率会更高缺点
:由于程序加载时就进行实例化,如果后续不再对此类进行任何操作,就会导致内存的浪费。
懒汉单例模式
懒汉模式的实例因为懒,所以只有有人要使用实例时才会去new,才不得不进行构建。
class Singleton
{
public:
static Singleton * GetInstance()
{
if(instance== nullptr)
{
instance = new Singleton();
}
return instance;//第一次进来再new,其他的时候直接返回的第一次创建的实例化对象
}
private:
static Singleton *instance;
Singleton(){}
Singleton(const Singleton & obj) = delete;
Singleton & operator=(const Singleton & obj) = delete;
};
//静态成员初始化
Singleton* Singleton::instance = nullptr;
void funa()
{
Singleton * instancea = Singleton::GetInstance();
cout<<instancea<<endl;
}
void funb()
{
Singleton* instanceb= Singleton::GetInstance();
cout<<instanceb<<endl;
}
int main()
{
thread thra(funa);
thread thrb(funb);
thra.join();
thrb.join();
return 0;
}
在我们看来即便运行结果没有问题,但是这个代码其实是线程不安全的。
因为发生了多次对象的构建,这与单利单例模式的初衷原则不符
——获取唯一实例化对象的方法不是可重入函数
(如果在多线程条件下没有发生竞态条件,就称这个函数是一个可重入函数)
详细解释:
在单线程中,这样的写法是可以正确使用的,但是在多线程中就不行了,该方法是线程不安全的。
(1)假如线程A和线程B, 这两个线程要访问getInstance函数,线程A进入getInstance函数,并检测if条件,由于是第一次进入,instance为空,if条件成立,准备创建对象实例。
(2)但是,线程A有可能被OS的调度器中断而挂起睡眠,而将控制权交给线程B。
(3)线程B同样来到if条件,发现instance还是为NULL,因为线程A还没来得及构造它就已经被中断了。此时假设线程B完成了对象的创建,并顺利的返回。
(4)之后线程A被唤醒,继续执行new再次创建对象,这样一来,两个线程就构建两个对象实例,这就破坏了唯一性。
另外,还存在内存泄漏的问题,new出来的东西始终没有释放
那么怎么解决这个问题呢??——在临界区代码段需要保证其原子操作:加入线程互斥锁
#include<mutex>
std::mutex mtx;//构建一个锁对象
class Singleton
{
public:
static Singleton * GetInstance()
{
//lock_guard<std::mutex> guard(mtx);
//考虑了多线程条件,没有考虑单线程-锁的粒度太大了导致单线程也需要不断的加锁解锁,于是放到里面
if(instance== nullptr)
{
lock_guard<std::mutex> guard(mtx);//不进入if就不会加锁解锁
instance = new Singleton();
}
return instance;//第一次进来再new,其他的时候直接返回的第一次创建的实例化对象
}
private:
static Singleton *instance;
Singleton(){}
Singleton(const Singleton & obj) = delete;
Singleton & operator=(const Singleton & obj) = delete;
};
//静态成员初始化
Singleton* Singleton::instance = nullptr;
仔细分析上述的代码还是存在线程安全的问题:
比如线程1进入获取到这把锁知乎,正在进行new操作,此时线程2也进入,阻塞到锁获取的位置,一旦线程1执行完毕,线程2就会继续执行,又生成一个对象。
我们的解决思路就是:锁+双重判断
以下就是线程安全的懒汉式单例模式:修改代码如下:
#include<mutex>
std::mutex mtx;//构建一个锁对象
class Singleton
{
public:
static Singleton * GetInstance()
{
if(instance== nullptr)
{
lock_guard<std::mutex> guard(mtx);
if(instance == nullptr)
{
instance = new Singleton();
}
}
return instance;
}
private:
//加上volatile这个关键字的目的,给指针添加,当某一线程改变其值时,其他线程马上就可以看到这个改变,
//因为线程已经不对这个共享变量进行缓存了,看到的都是内存的值,而非某一个寄存器
static Singleton *volatile instance;
Singleton(){}
Singleton(const Singleton & obj) = delete;
Singleton & operator=(const Singleton & obj) = delete;
};
//静态成员初始化
Singleton* Singleton::instance = nullptr;
此时我们发现:问题得到真正的解决
其实,目前更加流行懒汉式的下面这一种更加简洁的写法:也是线程安全的,并且没有使用互斥锁,它利用了static关键字的特性
。
class Singleton
{
public:
static Singleton* GetInstance()
{
static singleton instance;
return &instance;
}
private:
Singleton() {}
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
};
此时是线程安全的,因为这个实例在全局数据区分配内存,只有调动getinstance函数创建对象时初始化一次,更深一层
的解释是:编译器对于static静态局部变量的初始化,在汇编指令上已经自动加锁和解锁的互斥指令了。
体会:为什么要将这种设计模式定义为懒汉单例模式呢?
即:像一个懒汉一样,需要用到创建实例了程序再去创建实例,不需要创建实例程序就“懒得”去创建实例,这是一种时间换空间的做法,这体现了“懒汉的本性”。