一、单例模式
保证一个类仅有一个实例,并且提供一个该类的全局访问点
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.
1.1饿汉模式
在程序启动的时候,就已经把对象创建好了,它的实现比较简单,可以随时用随时拿,但是会影响程序的启动时间.
实现原理
-
构造函数私有化
-
在类中包含一个静态的类对象
-
提供一个静态的成员方法,返回静态对象的地址.
template <typename T>
class Singleton {
public:
// 局部变量只初始化一次,返回对象
static T* GetInstance() {
return &data;
}
private:
static T data;
};
只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例
参考文章:https://blog.csdn.net/lvyibin890/article/details/81943637
1.2懒汉方式
在程序需要的时候才创建,可以起到延迟加载,提高系统的启动速度
提供一个方法类
class Singleton{
public:
~Singleton(){}
static Singleton* getInstance();
static Singleton* m_instance;
private:
Singleton();
Singleton(const Singleton& other);
};
Singleton::m_instance = nullptr;
下面实现具体的方法
版本一: 线程不安全
Singleton* Singleton::getInstance(){
if(m_instance == nullptr){
m_instance = new Singleton();
}
return m_instance;
}
版本二: 加锁,但是代价太高
Singleton* Singleton::getInstance(){
Lock lock; // 获取锁,函数结束后锁自动释放
if(m_instance == nullptr){
m_instance = new Singleton();
}
return m_instance;
}
版本三:双检查锁,缓解了锁的代价,但是内存 reorder 不安全
new 方法的构造和赋值顺序可能颠倒
m_instance = new Singleton(); 这行代码按照我们正常的执行顺序是
- operator new 函数在内存划分一块地址
- 调用构造器,进行内存的初始化
- 然后把内存的首地址返回给 m_instance
但是 CPU 可能会把第2步和第3步调换顺序,还没有执行构造器就把地址返回给m_instance.这样一来,如果另一个线程也进入到函数内部,会返回m_instance,这个指针是不可以用的
Singleton* Singleton::getInstance(){
if(m_instance == nullptr){
// 如果没有第二个判断,这行代码可能会有多个线程执行到
Lock lock;
// 第二个判断不可以去掉,如果去掉,Lock 相当于摆设
if(m_instance == nullptr){
m_instance = new Singleton();
}
}
return m_instance;
}
版本四: 完美解决方案,在类中对 m_instance 进行声明 volatile static Singleton* m_instance;防止变量被编译器优化到寄存器
Singleton* Singleton::getInstance(){
if(m_instance == nullptr){
// 如果没有第二个判断,这行代码可能会有多个线程执行到
Lock lock;
// 第二个判断不可以去掉,如果去掉,Lock 相当于摆设
if(m_instance == nullptr){
m_instance = new Singleton();
}
}
return m_instance;
}
参考文章:https://blog.csdn.net/lvyibin890/article/details/81946863
二、单例模式的优点
-
在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
-
避免对资源的多重占用(比如写文件操作)
三、单例模式的缺点
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化
四、使用场景
-
要求生产唯一序列号。
-
WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来
-
创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。