什么是单例模式?
保证一个类只有一个实例,并提供一个访问它的全局访问点。首先,需要保证一个类只有一个实例;在类中,要构造一个实例,就必须调用类的构造函数,如此,为了防止在外部调用类的构造函数而构造实例,需要将构造函数的访问权限标记为protected或private;最后,需要提供要给全局访问点,就需要在类中定义一个static函数,返回在类内部唯一构造的实例。
要求:
(1)单例类保证全局只有一个自行创建的实例对象
(2)单例类提供获取这个唯一实例的接口
分类:
(1)懒汉模式:只有当调用对象时才创建实例,相对而言,实现代码较复杂但实用性强。
(2)饿汉模式:在执行main函数开始就调用创建对象。这种实现方式简单高效,但适用性受到限制。在main函数之前创建线程可能会出现问题,在动态库里程序可能崩溃。
懒汉模式
#include<iostream>
#include<mutex>
#include<Windows.h>
using namespace std;
class Singleton
{
public:
static Singleton* GetInstance()//获取唯一实例的接口函数
{
if (_inst == NULL)//创建实例
{
_inst = new Singleton;
}
return _inst;
}
void Print()
{
cout << "Singleton: " << this << endl;
}
private:
int _a;
Singleton()//将构造函数设为私有,防止在类外创建实例
:_a(0)
{}
static Singleton* _inst;//将指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例
};
Singleton* Singleton::_inst = NULL;//类外初始化静态成员
void Test1()
{
Singleton::GetInstance()->Print();
Singleton::GetInstance()->Print();
Singleton::GetInstance()->Print();
}
int main()
{
Test1();
system("pause");
return 0;
}
看上述结果,的确是只产生了一个实例对象,但如果在类外拷贝或是赋值呢?这又如何保证只产生一个实例呢?所以,我们还应该做到防拷贝。,即将拷贝构造和赋值运算符的重载声明为私有(类似于string 类防拷贝的实现)。
class Singleton
{
public:
static Singleton* GetInstance()//获取唯一实例的接口函数
{
if (_inst == NULL)//创建实例
{
_inst = new Singleton;
}
return _inst;
}
void Print()
{
cout << "Singleton: " << this << endl;
}
private:
int _a;
Singleton()//将构造函数设为私有,防止在类外创建实例
:_a(0)
{}
Singleton(const Singleton&);
Singleton& operator = (const Singleton&);
static Singleton* _inst;//将指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例
};
Singleton* Singleton::_inst = NULL;//类外初始化静态成员
上述过程我们看似已经实现了单例模式只能创建一个实例且只有一个唯一的接口函数,但是,这只是在单线程环境下,而如果有多线程同时访问的时候就会出现线程安全问题。在linux环境下,我们使用pthread_mutex加锁保证线程安全,这里我们同样采用加锁机制。
class Singleton
{
public:
static Singleton* GetInstance()//获取唯一实例的接口函数
{
_mtx.lock();//加锁,加锁期间其他线程不能访问临界区
if (_inst == NULL)//创建实例
{
_inst = new Singleton;
}
_mtx.unlock();//解锁
return _inst;
}
void Print()
{
cout << "Singleton: " << this << endl;
}
private:
int _a;
Singleton()//将构造函数设为私有,防止在类外创建实例
:_a(0)
{}
Singleton(const Singleton&);
Singleton& operator = (const Singleton&);
static Singleton* _inst;//将指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例
static mutex _mtx; // 保证线程安全的互斥锁
};
mutex Singleton::_mtx;//_mtx会调用mutex默认的无参构造函数,所以不用初始化
Singleton* Singleton::_inst = NULL;//类外初始化静态成员
而上述方案又存在一个问题,当我们加锁后申请资源时程序崩溃或是异常终止,而我们还没来得及释放锁资源,这时就会导致异常退出的线程一直拥有锁资源,而其他线程无法访问临界资源。这样既造成了死锁问题。
如何解决死锁问题呢?在学习只能智能指针时,我们了解过RAII机制(资源分配初始化), RAII利用构造函数分配并初始化资源,利用析构函数释放资源,保证原子性访问。所以这里我们使用RAII 机制自己“造轮子”。
class Lock
{
public:
Lock(mutex& mtx)
:_mtx(mtx)
{
_mtx.lock();
}
~Lock()
{
_mtx.unlock();
}
protected:
Lock(const Lock&);
Lock& operator = (const Lock&);
private:
mutex& _mtx;
};
class Singleton
{
public:
static Singleton* GetInstance()//获取唯一实例的接口函数
{
Lock lock(_mtx);//RAII机制
if (_inst == NULL)//创建实例
{
_inst = new Singleton;
}
return _inst;
}
void Print()
{
cout << "Singleton: " << this << endl;
}
private:
int _a;
Singleton()//将构造函数设为私有,防止在类外创建实例
:_a(0)
{}
static mutex _mtx; // 保证线程安全的互斥锁
Singleton(const Singleton&);
Singleton& operator = (const Singleton&);
static Singleton* _inst;//将指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例
};
Singleton* Singleton::_inst = NULL;//类外初始化静态成员
mutex Singleton::_mtx;//_mtx会调用mutex默认的无参构造函数,所以不用初始化
而实际上,库中是有这样的加锁机制的,我们可以直接调用库函数进行加锁。
lock_guard<mutex>lock(_mtx);
我们的单例模式要求是线程安全且高效,现在我们已经实现了线程安全,那么高效如何实现?需要使用双检查。,而在上述过程中,我们创建实例时,使用
_inst = new Singleton
这行代码,系统执行时,会调用operator new 、构造函数及赋值运算符,但在不同的编译器下,可能会对它进行不同程度的优化,这样我们就无法保证内部的调用顺序,因此我们对此优化,并加入内存栅栏。
那么既然我们使用了new开辟空间,自然是要delete释放空间的。在实际项目中,我们其实并不关心单例模式的释放,因为全局就一个实例,它一直伴随着这个软件的使用,可以说它的生命周期随软件。当这个软件停止了,那这个实例自然也就没有了。但有些情况是必须我们手动来释放资源的:
(1)在类中,有一些文件锁,文件句柄,数据库连接等等,这些随着程序的关闭而不会立即关闭的资源,必须要在程序关闭前,进行手动释放。
(2)有强迫症的程序员(我觉得程序员一般都有强迫症的哈哈)
基于以上的分析和不断优化的过程,我们给出完整版的懒汉模式的实现:
class Singleton
{
public:
static Singleton* GetInstance()//获取唯一实例的接口函数
{
if (_inst == NULL)//双检查机制,只有创建实例的时候才进行加锁解锁来提高代码效率
{
//Lock lock(_mtx);//RAII机制
lock_guard<mutex>lock(_mtx);
if (_inst == NULL)//创建实例
{
Singleton* tmp = new Singleton;
MemoryBarrier();//内存栅栏
_inst = tmp;
}
}
return _inst;//返回实例化的唯一对象
}
void Print()
{
cout << "Singleton: " << _inst << endl;
}
static void DellInstance()//在某些情况才需要释放
{
lock_guard<mutex> lock(_mtx);//防止对象被释放多次
if (_inst)
{
cout << "delete" << endl;
delete _inst;
_inst = NULL;//防止野指针的出现
}
}
struct GC
{
~GC()
{
DellInstance();
}
};
private:
int _a;
Singleton()//将构造函数设为私有,防止在类外创建实例
:_a(0)
{}
~Singleton()
{
}
static mutex _mtx; // 保证线程安全的互斥锁
Singleton(const Singleton&);
Singleton& operator = (const Singleton&);
static Singleton* _inst;//将指向实例的指针定义为静态私有,这样定义静态成员函数获取对象实例
};
Singleton* Singleton::_inst = NULL;//类外初始化静态成员
mutex Singleton::_mtx;//_mtx会调用mutex默认的无参构造函数,所以不用初始化
static Singleton::GC gc;
void Test1()
{
Singleton::GetInstance()->Print();
Singleton::GetInstance()->Print();
Singleton::GetInstance()->Print();
Singleton::DellInstance();
//atexit(Singleton::DellInstance);//也注册回调函数,在main函数之后调用析构
}
int main()
{
Test1();
system("pause");
return 0;
}
饿汉模式
—-饿汉模式1
class Singleton
{
public:
static Singleton& GetInstance()
{
static Singleton inst;//静态变量只会创建一次
return inst;
}
void Print()
{
cout << "Singleton:" << this << endl;
}
protected:
Singleton()
:_a(0)
{}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
int _a;
};
void Test1()
{
Singleton::GetInstance().Print();
Singleton::GetInstance().Print();
Singleton::GetInstance().Print();
}
int main()
{
Test1();
system("pause");
return 0;
}
饿汉模式2
#include<assert.h>
class Singleton
{
public:
static Singleton& GetInstance()
{
assert(_inst);
return *_inst;
}
void Print()
{
cout << "Singleton:" << _a << endl;
}
protected:
Singleton()
:_a(0)
{}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
static Singleton* _inst;
int _a;
};
Singleton* Singleton::_inst = new Singleton;//静态成员在main函数之前初始化
void Test()
{
Singleton::GetInstance().Print();
Singleton::GetInstance().Print();
Singleton::GetInstance().Print();
}
int main()
{
Test();
system("pause");
return 0;
}
总结:懒汉式的特点是用到实例化的时候才会加载,而饿汉式是一开始就加载了。两种方案的构造函数和公用方法都是静态的(static),实例和公用方法又都是私有的(private)。但是饿汉式每次调用的时候不用做创建,直接返回已经创建好的实例。这样虽然节省了时间,但是却占用了空间,实例本身为static的,会一直在内存中带着。懒汉式则是判断,在用的时候才加载,会影响程序的速度。
推荐博客:单例模式 博主举的例子有助于大家理解单例模式。