单例模式
- 在一个项目中,全局范围内,某个类的实例有且仅有一个,通过这个唯一的实例可以提供数据给其他模块访问,这种模式就是单例模式。
- 典型应用为任务队列
- 不使用全局变量保证了安全性
- 单例模式并不仅仅是指在全项目仅new一个对象,还需要在定义类的同时杜绝可以定义新的实例的所有方法
如何实现单例模式
一个类对于一个对象的操作有
- 构造函数:创建一个新的对象
- 拷贝构造函数:根据已有的对象拷贝出一个新的对象
- 拷贝赋值操作符重载函数:两个对象之间的赋值
要杜绝对象的实例化需要解决构造函数以及拷贝构造函数
需要进行以下操作
- 构造函数私有化或使用=delete禁用。私有化时需要注意私有化后仅能保证类外部无法通过对象调用构造函数,还需要再次基础上将类内部这个唯一的对象定义为静态类,且将该静态对象的权限设置为私有的,通过类名访问静态属性或方法。类中仅有静态函数可以访问静态成员变量,因此还需要构造一个静态的成员函数用于对该唯一的对象进行访问。
- 拷贝构造函数私有化或使用=delete禁用
- 拷贝赋值操作符重载函数私有化或使用=delete禁用
举例
//设置为私有成员函数
class Singleton{
public:
static Singleton* getInstance(){
return m_singleone;
}
private:
Singleton() = {};
Singleton(const Singleton& t) = {};
Singleton& operator=(const Singleton& obj) = {};
static Singleton* m_singleone;//静态变量无法在内部初始化
};
Singleton* Singleton::m_singleone = new Singleton;//仍在作用域下(私有变量的作用域)
int main(){
Singleton* SingP = Singleton::getInstance();
}
//使用delete
class Singleton{
public:
Singleton() = delete;//无法使用
Singleton(const Singleton& t) = delete;
Singleton& operator=(const Singleton& obj) = delete;//拷贝幅值操作符重载函数
private:
}
注:只是不能新建实例对象,可以构造该类的指针指向唯一的实例对象,并且通过类的getInstance()访问
饿汉模式
在定义类的时候创建单例对象,如上述
懒汉模式
使用时才创建单例对象
即
//设置为私有成员函数
class Singleton{
public:
static Singleton* getInstance(){
if(m_singleone == nullptr){
m_singleone = new Singleton;
}
else{
}
return m_singleone;
}
private:
Singleton() = {};
Singleton(const Singleton& t) = {};
Singleton& operator=(const Singleton& obj) = {};
static Singleton* m_singleone;//静态变量无法在内部初始化
};
Singleton* Singleton::m_singleone = nullptr;//仍在作用域下(私有变量的作用域)
int main(){
Singleton* SingP = Singleton::getInstance();
}
双重检查锁定
- 多线程时饿汉模式没有线程安全问题,懒汉模式可能会出现同时访问单例(即并发时)对象创建多个单例对象的问题(使用互斥锁,顺序访问效率较低)
- 解决:使用双重检查锁定解决,通过互斥锁防止同一时刻不同线程创建实例如
//设置为私有成员函数
class Singleton{
public:
static Singleton* getInstance(){
if(m_singleone == nullptr){
m_mutex.lock();
if(m_singleone == nullptr){
m_singleone = new Singleton;
}
m_mutex.unlock();
}
else{
}
return m_singleone;
}
private:
Singleton() = {};
Singleton(const Singleton& t) = {};
Singleton& operator=(const Singleton& obj) = {};
static Singleton* m_singleone;//静态变量无法在内部初始化
static mutex m_mutex;//静态成员变量都需要进行申明
};
Singleton* Singleton::m_singleone = nullptr;//仍在作用域下(私有变量的作用域)
mutex Singleton::m_mutex;
int main(){
Singleton* SingP = Singleton::getInstance();
}
- 由于语句编码后机器指令的执行顺序会进行重排,多线程时可能会出现第一个线程还没有实例化空指针处的对象,另一个线程同时对该对象进行了访问,此时寄
可以使用原子变量automic进行管理,通过store存储单例对象,通过load访问单例对象
//设置为私有成员函数
class Singleton{
public:
static Singleton* getInstance(){
Singleton* temp = m_singleone.load()
if(temp == nullptr){
m_mutex.lock();
if(temp == nullptr){
temp = new Singleton;
m_singleone.store(temp);
}
m_mutex.unlock();
}
else{
}
return temp;
}
private:
Singleton() = {};
Singleton(const Singleton& t) = {};
Singleton& operator=(const Singleton& obj) = {};
//static Singleton* m_singleone;//静态变量无法在内部初始化
static automic<Singleton*> m_singleone;//静态变量无法在内部初始化
static mutex m_mutex;//静态成员变量都需要进行申明
};
//Singleton* Singleton::m_singleone = nullptr;//仍在作用域下(私有变量的作用域)
static automic<Singleton*> m_singleone;
mutex Singleton::m_mutex;
int main(){
Singleton* SingP = Singleton::getInstance();
}
在原子变量中这两个函数在处理指令的时候默认的原子顺序是memory_order_seq_cst(顺序原子操作 - sequentially consistent),使用顺序约束原子操作库,整个函数执行都将保证顺序执行,并且不会出现数据竞态(data races),不足之处就是使用这种方法实现的懒汉模式的单例执行效率更低一些。
- 原子操作顺序:在普通变量中,多数据并发的访问数据可能会导致数据竞争,这可能会导致数据操作过程的顺序错误,automic可以通过指定不同的memory orders控制对原子对象的访问顺序和可见性。原子变量可用于确保对共享标量的操作在执行时不会被其他线程的操作干扰,从而避免竞态条件和死锁等问题。
静态局部对象
该方法无需对实例对象先进性初始化,属于懒汉模式,且实例对象一直以static变量类型存储于全局数据区域
//设置为私有成员函数
class Singleton{
public:
static Singleton* getInstance(){
static Singleton temp;//只有当申请内存,指定指针并将数据写入该内存才为完成初始化
return &temp;
}
private:
Singleton() = {};
Singleton(const Singleton& t) = {};
Singleton& operator=(const Singleton& obj) = {};
};
int main(){
Singleton* SingP = Singleton::getInstance();
}
该方法线程安全基于C+11的特性
如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当灯带该变量完成初始化
总结
饿汉模式可以避免考虑线程安全问题,但会再最开始占用内存空间。
懒汉模式在第一次访问实例对象时才进行唯一实例进行初始化,多线程时需考虑线程安全问题。在计算机内存足够时可以直接使用饿汉模式。