单例模式定义
保证一个类仅有一个实例,并提供一个该实例的全局访问点。
从初始化的时间角度上看,单例模式分为两种,懒汉模式与饿汉模式
懒汉模式与饿汉模式
单例模式需要将构造和析构私有化,让其他用户不能调用; 例如拷贝,移动等构造函数都不能被调用
懒汉模式
class Singleton {
public:
static Singleton * GetInstance() {
//在使用的时候才会初始化单例对象,并且只初始化一次
if (_instance == nullptr) {
_instance = new Singleton();
}
return _instance;
}
private:
Singleton(){}; //构造
~Singleton(){};
Singleton(const Singleton &) = delete; //拷⻉ 构造
Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造
Singleton(Singleton &&) = delete;//移动构造
Singleton& operator=(Singleton &&) = delete;//移动拷贝构造
static Singleton * _instance;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化
饿汉模式
class Singleton {
public:
static Singleton * GetInstance() {
return _instance;
}
private:
Singleton(){}; //构造
~Singleton(){};
Singleton(const Singleton &) = delete; //拷⻉ 构造
Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造
Singleton(Singleton &&) = delete;//移动构造
Singleton& operator=(Singleton &&) = delete;//移动拷贝构造
static Singleton * _instance;
};
Singleton* Singleton::_instance = new Singleton();//静态成员需要初始化即生成单例对象
懒汉模式和饿汉模式的区别
直观区别:
饿汉:饿汉模式就是类在加载时,单例初始化便完成,保证GetInstance的时候,单例是已经存在了。
懒汉:懒汉比较懒,只有当调用GetInstance的时候,才会初始化单例对象。性能上的区别:
饿汉:在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成。
懒汉:由于会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉模式一样了。
如果这个创建过程很耗时,比如需要连接多次数据库、传输大量文件等;并且这个类还并不一定会被使用,那么这个创建过程就是无用的。此时懒汉会比饿汉更适合,具体那个模式更好还要看应用场景决定。线程安全:
饿汉:是线程安全的,可以直接用于多线程而不会出现问题。懒汉:本身是非线程安全的。
懒汉模式的不同版本
版本一
class Singleton {
public:
static Singleton * GetInstance() {
if (_instance == nullptr) {
_instance = new Singleton();
}
return _instance;
}
private:
Singleton(){}; //构造
~Singleton(){};
Singleton(const Singleton &) = delete; //拷⻉ 构造
Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造
Singleton(Singleton &&) = delete;//移动构造
Singleton& operator=(Singleton &&) = delete;//移动拷贝构造
static Singleton * _instance;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化
存在问题:
_instance是静态全局区分配的内存。程序退出时_instance这个指针 可以释放;但是_instance这个指针指向的堆上的资源(new Singleton())是没法回收的,没有delete。注意:delete也不能放在析构函数中,因为delete会调用析构函数,析构函数又有delete,又调用析构函数,导致无限递归析构。
问题1:上面的程序在退出时,不会去析构_instance。造成内存泄漏,可以使用unique_ptr智能指针解决
问题2:线程不安全
版本二
class Singleton {
public:
static Singleton * GetInstance() {
if (_instance == nullptr) {
_instance = new Singleton();
atexit(Destructor);//使用atexit调用Destructor函数释放内存
}
return _instance;
}
private:
static void Destructor() {
if (nullptr != _instance) { //
delete _instance;
_instance = nullptr;
}
}
Singleton(){}; //构造
~Singleton(){};
Singleton(const Singleton &) = delete; //拷⻉构造
Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造
Singleton(Singleton &&) = delete;//移动构造
Singleton& operator=(Singleton &&) = delete;//移动拷贝构造
static Singleton * _instance;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化
// 还可以使⽤ 内部类,智能指针来解决; 但此时还有线程安全问题
存在问题:在多线程下,可能两个线程同时进入 if (_instance == nullptr)指令,同时初始化单例线程,造成线程不安全
版本三
#include <mutex>
class Singleton { // 懒汉模式 lazy load
public:
static Singleton * GetInstance() {
if (_instance == nullptr) {
std::lock_guard<std::mutex> lock(_mutex); // 3.2双重检测可以避免第一次读的时候多个线程进入的问题。
if (_instance == nullptr) {
_instance = new Singleton();
//加锁只是考虑C++98时单核时代的多线程情况,而C++11的多核时代可能会有重排机制,同样会有线程安全问题
atexit(Destructor);
}
}
return _instance;
}
private:
static void Destructor() {
if (nullptr != _instance) {
delete _instance;
_instance = nullptr;
}
}
Singleton(){}; //构造
~Singleton(){};
Singleton(const Singleton &) = delete; //拷⻉构造
Singleton& operator=(const Singleton&) = delete;//拷贝赋值构造
Singleton(Singleton &&) = delete;//移动构造
Singleton& operator=(Singleton &&) = delete;//移动拷贝构造
static Singleton * _instance;
static std::mutex _mutex;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化
std::mutex Singleton::_mutex; //互斥锁初始化
通过加锁来解决多线程时的线程安全问题,是可以的。
但是执行operator new命令时的步骤:
- 分配内存空间
- 调用构造函数初始化对象
- 返回指针,也就是设置_instance指向刚刚分配的内存的地址
其中2和3步骤没有依赖关系,可以先初始化对象,再设置指针指向地址;也可以先设置地址,再初始化对象。
如果是单线程下调用顺序1->2->3,没有问题
但是c++11的多核时代会有一些优化操作,例如编译器重排 ,CPU重排等;
重排机制可能会违反顺序一致性
可能会调用顺序1->3->2。因此当执行完步骤3后,_instance已经不为空了,这时虽然还没有初始化,但是其他线程已经可以使用这个还没有初始化的对象了,也造成线程安全问题。
存在问题:执行new Singleton()时,重排优化机制导致的线程安全问题。
版本四
#include <iostream>
using namespace std;
class Singleton
{
public:
static Singleton* GetInstance() {
static Singleton* instance = new Singleton();
return instance;
}
private:
Singleton(){}; //构造
~Singleton(){};
Singleton(const Singleton &) = delete; //拷⻉ 构造
Singleton& operator=(const Singleton&) =
delete;//拷贝赋值构造
Singleton(Singleton &&) = delete;//移动构造
Singleton& operator=(Singleton &&) =
delete;//移动拷贝构造
};
int main() {
Singleton* test1 = Singleton::GetInstance();
cout << test1 << endl;
Singleton* test2 = Singleton::GetInstance();
cout << test2 << endl;
return 0;
}
// 继承 Singleton
// ubuntu下使用使用C++11新特性编译程序方式:g++ Singleton.cpp -o singleton -std=c++11
该版本具备的优点:
- 利⽤静态局部变量特性,延迟加载;
- 利⽤静态局部变量特性,系统⾃动回收内存,⾃动调⽤析构函 数;
- 静态局部变量初始化时,没有 new 操作带来的cpu指令 重排reorder操作;在C++11之后,局部静态变量是线程安全的。
- c++11 静态局部变量初始化时,具备线程安全;