懒汉模式:
如果单例对象构造十分耗时或者占用很多资源,比如加载插件, 初始化网络连接,读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
首先,有一个极其简单的单线程懒汉模式,初步理解它的思想:
// 单线程懒汉模式
class Singleton
{
public:
static Singleton* getInstance() {
// 做饭
// 提高后续线程调用接口的效率
if (_sin == nullptr) { // 第一次为空,创建对象,第二次非空,直接返回,保证单例
_sin = new Singleton;
}
return _sin;
}
private:
// 1. 构造函数私有化 2. 拷贝构造私有化(不必实现) 3. 赋值运算符无所谓私有化,因为其不创建新的对象
Singleton(){}
Singleton(const Singleton& s) = delete;
// static Singleton _sin; // 启动之前肯定需要进行初始化,东西已经准备好了,为饿汉模式
static Singleton* _sin; // 定义为指针,与对象不为同一类型,其为单独的指针类型
};
Singleton* Singleton::_sin = nullptr;
进程、单线程、多线程
一个可执行的程序就是一个进程,在一个进程内可创建多个执行流,也称为线程。即在进程内只有一个执行流称为单线程程序。相当于原来是一个人干事情,变成很多人干同一件事情,单进程是一个串行流。例如:双11、双12,开启秒杀活动时,排队进行串行流可能连页面都进不去,然而现实中并没有这么夸张。
实际上,这里面都是些高并发的操作,涉及到多线程操作,你的请求不是一个线程去执行,而是多个或者一批线程去执行,提高了总体的效率,相当于一开始只有一个人处理你的请求,现在一批人帮着去处理。如生活中常见的是银行柜台,以前可能只有一个柜台,需要排队且队伍很长,现在有多个柜台,但也需要排队但队伍很短也就是提高了效率。 即有多个执行流时,会大大提高效率。每一个进程都有一个进程地址空间,且不共享,即不同的进程有不同的进程地址空间。但一个进程有多个线程的话,线程会共享该进程的地址空间,它们之间访问的内存可能是同一片内存。相当于出现你和很多人同时在修改一篇文章,预测值与实际值大相径庭。
int main() {
int i = 0;
i++; // 针对 ++ 指令
return 0;
}
//i++;
//009F32CF mov eax, dword ptr[i]
//009F32D2 add eax, 1
//009F32D5 mov dword ptr[i], eax
对于++指令来讲,若有两个线程同时执行该指令,期望值为2,但是它仍可能是1,。在此其实为“同时写”的问题。其本来为“串行”的操作,执行两次。 在此引入“原子操作”,对于最小的执行指令的单位,可以理解为一条指令,其不会在CPU中被打乱,即说的一句话不会被打乱,但多句话可能会被打乱。 下面是针对i++的反汇编,虽然只有一行代码,但在底层实现上其有三步操作。假如有两个线程去改变该值,eax是寄存器,假如为多核的寄存器,每一个核上均有一个线程去执行,每一个CPU都有一个寄存器,两个寄存器对两个核进行一个++的操作,那么两个核就有两个串。第一个线程将i放入寄存器中,现在为0,紧接着第二个线程再去拿这个值,i依旧还是0,第一个线程执行完之后将1写入i中,但是当第二个线程执行完毕后,又将1写入i中,导致i最后结果为1。但是也有可能为2,即第一个线程执行完毕后,第二个线程再进行执行。但是这一切全靠CPU的时间分配,CPU实际上是一个“笼形”的操作,线程之间无法确定先后,会产生线程安全问题,写的时候可能会写错,不会达到预期的效果。而饿汉模式没有线程安全的问题,因为其“只读”,不对其进行修改。但是“懒汉模式”因为它为创建为主,可能会多个线程创建出多个对象。
void fun() {
for (int i = 0; i < 10; i++) {
cout << i << endl;
}
}
#include <thread> // C++11 加入线程库,C++98没有 <pthread> Linux中的系统库,两者均可以使用线程
int main() {
int i = 0;
thread t1(fun); // main为主线程
thread t2(fun); // 创建了t1、t2两个线程
t1.join(); // 相当于 会和 的意思,让主线程等待上面两个线程执行完自己的命令一起结束
t2.join();
system("pause");
return 0;
}
对于多线程操作,其是没有一定的顺序的,是一种随机的操作。
当两个线程执行到getlnstance有三种情况,t1先到、t2先到、同时到。当为同时到时无法判断哪个对象是谁new出来的,那么返回对象的地址也不知道是哪个创造出来的,即无法保证“单例”前提,后创建的对象很可能覆盖掉前一个的地址,导致前一个对象内存丢失极易造成内存泄露,危害极大。
void testsingleton() {
cout << Singleton::getInstance() << endl; // 测试双线程调用getInstance创建对象的地址
}
int main() {
thread t1(testsingleton); // 建立两个线程
thread t2(testsingleton);
system("pause");
return 0;
}
#include <mutex> // 加锁头文件,互斥锁,所有的线程共用同一把锁,全局只有一把锁,用一把锁限制所有线程
private:
static mutex _mtx; // 全局只有一把锁,限制全部线程
static Singleton* getInstance() {
_mtx.lock(); // 加锁不能在if内,没有意义,还是要创建对象
if (_sin == nullptr) { // 第一次为空,创建对象,第二次非空,直接返回,保证单例
_sin = new Singleton;
}
_mtx.unlock();
}
请仔细思考一下:这样加锁的方式是能够保证对象创建单一,不会造成内存泄露。但是,每一个线程进来之后都得被锁住,再判断,再解锁,效率大大降低。加锁是为了创建一次对象,对象创建好之后就不需要走判断逻辑了,下一次线程进来之后直接返回地址即可,可以大幅度提高效率。
static Singleton* getInstance() {
if (_sin == nullptr) {
_mtx.lock(); // 加锁不能在if内,没有意义,还是要创建对象
if (_sin == nullptr) { // 第一次为空,创建对象,第二次非空,直接返回,保证单例
_sin = new Singleton;
}
_mtx.unlock();
}
return _sin;
}
在调用getInstance时,用new申请了空间,但用完我们并没有释放空间。现在,也不需要手动去释放,单例不仅仅在一个地方使用,可能也在其它地方使用,释放了会导致程序崩溃。
Singleton* ps = Singleton::GetInstance();
delete ps;
ps = nullptr;
所以我们只能delete ps,再将ps置空,但是,将ps空间释放之后,类中的空间又没有被释放,还是一个有效值。而且不光在此使用这个空间,在其他的地方到该空间的接口,一开始调用的时候该指针有效,但在此已经被释放了,会出现解引用的错误。不能手动去释放。
在此,一般可以不用管,因为其为静态成员,在整个程序运行周期内均有效,程序运行结束即进程结束,那么会将所有的空间资源均返还给系统,达到垃圾回收的目的。
但是若是想手动释放的话,可以在内部定义一个内部类辅助操作。内部类可以访问外部类的私有成员,并且可以直接访问。
class Singleton
{
public:
static Singleton* getInstance() {
// 做饭
// 提高后续线程调用接口的效率
if (_sin == nullptr) {
_mtx.lock(); // 加锁不能在if内,没有意义,还是要创建对象
if (_sin == nullptr) { // 第一次为空,创建对象,第二次非空,直接返回,保证单例
_sin = new Singleton;
}
_mtx.unlock();
}
return _sin;
}
class GC { // 定义内部类,进行垃圾回收
public:
~GC() {
if (_sin) {
delete _sin;
_sin = nullptr;
}
}
};
private:
// 1. 构造函数私有化 2. 拷贝构造私有化(不必实现) 3. 赋值运算符无所谓私有化,因为其不创建新的对象
Singleton(){}
Singleton(const Singleton& s) = delete;
// static Singleton _sin; // 启动之前肯定需要进行初始化,东西已经准备好了,为饿汉模式
static Singleton* _sin; // 定义为指针,与对象不为同一类型,其为单独的指针类型
static mutex _mtx; // 全局只有一把锁,限制全部线程
static GC _gc;
};
Singleton* Singleton::_sin = nullptr;
mutex Singleton::_mtx;
Singleton::GC Singleton::_gc; // 它是静态成员,其生命周期也是整个程序的生命周期,调用析构函数释放空间
为什么要采用内部类来做这样一个事情呢?为什么不能在单例上直接写析构函数进行资源回收呢?
class Singleton
{
public:
static Singleton* getInstance() {
// 做饭
// 提高后续线程调用接口的效率
if (_sin == nullptr) {
_mtx.lock(); // 加锁不能在if内,没有意义,还是要创建对象
if (_sin == nullptr) { // 第一次为空,创建对象,第二次非空,直接返回,保证单例
_sin = new Singleton;
}
_mtx.unlock();
}
return _sin;
}
~Singleton() { // 单例中析构函数产生递归效果
if (_sin) { // 之前所产生的对象不为当前类,不会重复递归调用析构函数
delete _sin;
_sin = nullptr;
}
}
~Singleton,会在delete _sin上重复调用析构函数产生递归效应。因为之前调用析构函数释放的资源不是当前类类型的,不会去递归调用当前类的析构函数,而再次刚好触发了该条件。
懒汉模式完整版:
// 懒汉
// 优点:第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制。
// 缺点:复杂
// 五大实现要点
// 1. 构造函数私有
// 2. 封死拷贝构造
// 3. 提供静态线程安全的接口(double-check,提高效率)
// 4. 定义一个静态单例类型的指针,初始化为nullptr
// 5.(可选)定义一个内部类,辅助释放单例指针
#include <iostream>
#include <mutex>
#include <thread> // C++11中的线程库 <pthread> Linux下线程...
using namespace std;
class Singleton
{
public:
static Singleton* GetInstance() {
// 注意这里一定要使用Double-Check的方式加锁,才能保证效率和线程安全
if (nullptr == m_pInstance) {
m_mtx.lock(); // 加锁在if外部,注意是否为有效锁
if (nullptr == m_pInstance) {
m_pInstance = new Singleton();
}
m_mtx.unlock();
}
return m_pInstance;
}
// 建立内部类,实现一个内嵌垃圾回收类
class CGarbo {
public:
~CGarbo() {
if (Singleton::m_pInstance)
delete Singleton::m_pInstance;
}
};
// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
static CGarbo Garbo;
private:
// 构造函数私有
Singleton() {};
// 防拷贝
Singleton(Singleton const&);
Singleton& operator=(Singleton const&);
static Singleton* m_pInstance; // 单例对象指针
static mutex m_mtx; //互斥锁
};
Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Garbo;
mutex Singleton::m_mtx;
void func(int n)
{
cout << Singleton::GetInstance() << endl;
}
// 多线程环境下演示上面GetInstance()加锁和不加锁的区别。
int main()
{
thread t1(func, 10);
thread t2(func, 10);
t1.join();
t2.join();
cout << Singleton::GetInstance() << endl;
cout << Singleton::GetInstance() << endl;
system("pause");
}
void MemoryLeaks()
{
// 1.内存申请了忘记释放
// 理论上申请了内存没有进行释放导致内存泄露,实际上程序运行完之后,
// 栈会将所有资源全部还给系统
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}