什么是单例模式
单例模式是非常典型的一种设计模式
某些类, 只应该具有一个对象(实例), 就称之为单例.
一个类实例化的对象公用同一份资源(一份资源只能被申请一次)
例如:一个男人只能有一个媳妇.
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.
饿汉方式
资源在程序初始化的时候就去加载。(后边使用的时候就能直接使用)
优缺点:
- 使用的时候比较流畅
- 有可能会加载用不上的资源,并且会导致程序初始化的时间比较慢
只要通过 Singleton 这个包装类来使用 T 对象,则一个进程中只有一个 T 对象的实例。
懒汉方式
资源在使用的时候发现还没有加载,则申请加载
优缺点:
- 程序初始化比较快
- 第一次运行某个模块的时候就会比较慢,因为这时候去加载相应资源
懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度.
[举个洗碗的例子说明]
- 吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
- 吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.
如何实现单例模式
- 饿汉:单例类定义的时候就进行实例化。
类加载速度相比懒汉慢,但获取对象的速度快,是一种典型的以时间换取空间的做法
- 优点:线程安全
- 缺点:不管你用不用这个对象,他都会先创建出来,会造成浪费内存空间
使用static就可以将一个成员变量设置为静态变量,则所有对象公用一份资源(保证对象的唯一性),并且在程序初始化的时候就会申请资源(静态成员变量初始化在类外)
class Singleton
{
private:
Singleton(){}
static Singleton* instance;
public:
static Singleton* GetSingleton()
{
return instance;
}
};
Singleton* Singleton::instance = new Singleton();
- 懒汉:第一次用到类的实例的时候才回去实例化。
单线程:
函数使用static,保证仅仅有一个实例被创建。
class Singleton {
Singleton(){} // 构造函数私有化,不允许外界创建对象
static Singleton* data;
public:
static Singleton* GetInstance() {
// 只有 data 为NULL时才创建一个实例以避免重复创建
if (data == NULL) {
data = new Singleton();
}
return data;
}
};
多线程实现所注意的细节:
- 使用static保证所有对象使用同一份资源
- 使用volatile,防止编译器过度优化(防止多线程下对代码优化造成的不当影响)
- 实现线程安全,保证资源判断以及申请过程是安全的
- 外部二次判断,以及避免资源已经加载成功每次获取都要加锁解锁,以及所带来的锁冲突
class Singleton {
volatile static Singleton* inst;
static std::mutex _mutex;
public:
static Singleton* GetInstance() {
if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.
_mutex.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.(实现线程安全)
if (inst == NULL) {
inst = new Singleton();
}
_mutex.unlock();
}
return inst;
}
};
不使用 volatile ,可能造成的不当影响:
主要在于inst = new Singleton();
这句,这并非是一个原子操作,事实上这句话大概做了下面 3 件事情。
- 给 inst 分配内存
- 调用 Singleton 的构造函数来初始化成员变量,形成实例
- 将inst对象指向分配的内存空间(执行完这步 inst 才是 非null 了)
在编译器中存在指令重排序的优化。
也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 inst 已经是非 null 了(但却没有初始化),所以线程二会直接返回 inst,然后使用,然后顺理成章地报错。
再稍微解释一下,就是说,由于有一个『inst已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (inst ==null)
这里,这里读取到的inst已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。这里的关键在于线程T1对instance的写操作没有完成,线程T2就执行了读操作。
对于此出现的问题,解决方案为:给instance的声明加上volatile关键字
知识点习题:
- 关于单例模式,如何隐藏构造函数
A. 使用protected
B. 写注释
C. 不声明
D. 声明纯虚函数
正确答案: A
答案解析:
private构造函数的问题就是间接剥夺了被继承的可能,如果这样 建议把类型标记为密封的
如果不想剥夺被继承的能力,那么就使用protected吧
如果本篇博文有帮助到您,请留个赞激励博主呐