C++特殊类的设计: 单例模式 (饿汉 + 懒汉)

单例模式是C++中很重要的一种设计模式, 这次我们就来聊聊单例模式的设计, 从饿汉和懒汉两种模式出发去实现单例模式,

实现单例模式之前, 建议大家先看一看其他C++中的特殊类的设计, 有关后面的设计思想

这里附上链接: C++特殊类的设计: 只能在堆/栈上创建对象, 不能被继承的类

文章目录

  • 单例模式
  • 饿汉模式
  • 懒汉模式
    • 线程安全


单例模式

很简单, 设计一个类, 只能创建一个对象就是单例模式


饿汉模式

饿汉模式: 保证程序启动前, 对象就存在

要想使对象在程序启动前就存在, 我们可以用静态成员

静态成员在main函数执行之前就已经存在

要想实现单例模式, 首先我们不能让他随意调用构造和拷贝构造函数
构造函数私有化 + 防拷贝 (设置拷贝构造为delete)
然后根据上面说的, 我们要在类中定义一个静态成员变量, 也就我们要的唯一的对象
注意静态成员要在类外进行初始化, 初始化静态成员就即调用了构造函数
然后我们还要定义一个静态的公有接口, 返回我们的静态对象
注意一定要返回引用, 因为拷贝构造已经被设置成delete, 无法返回值

具体代码如下:

//1. 饿汉模式: 保证程序启动前, 对象就存在
class Singleton {
public:
	//公有的静态方法: 获取唯一的静态对象
	static Singleton& getojb() {
		return _sl;
	}
private:
	//构造函数私有化
	Singleton() {
		cout << "Singleton" << endl;
	}

	//防拷贝
	Singleton(const Singleton&) = delete;
	
	//静态成员
	static Singleton _sl;
};

//静态数据先于主函数存在
//静态成员初始化, 在主函数之前调用构造
Singleton Singleton::_sl;

void test3() {
	Singleton& ref = Singleton::getojb();
}

饿汉模式的优缺点也很明显

  • 优点 : 简单, 容易实现
  • 缺点 : 可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。

懒汉模式

懒汉模式: 使用的时候再创建 (延迟加载)

首先还是禁止随意调用构造和拷贝构造
构造函数私有化 + 防拷贝 (设置拷贝构造为delete)
然后提供一个公有接口, 只在第一次调用时创建对象并返回
那么要怎么做呢?
我们可以定义一个静态指针, 唯一标识一块空间, 初始化为空
每次判断指针是否为空, 为空则标识第一次创建对象, 不为空则不创建

代码如下:

//2. 懒汉模式: 使用的时候再创建
class Singleton2 {
public:
	//静态公有方法, 只有第一次调用时创建对象
	static Singleton2* getobj() {
		//判断标记指针是否为空, 空则创建对象, 不为空就直接返回
			if (_ptr == nullptr) {
				_ptr = new Singleton2;
			}
		return _ptr;
	}
private:
	//构造函数私有化
	Singleton2() {
		cout << "Single()" << endl;
	}

	//防拷贝
	Singleton2(const Singleton2&) = delete;

	//静态指针, 标记空间
	static Singleton2* _ptr;
};
//初始化静态指针
Singleton2* Singleton2::_ptr = nullptr;

线程安全

上述代码乍一看么得问题, 但是如果放在多线程的情况下

所有线程"同时"进入到这个函数, 发现指针都是空, 那么每个线程都会申请一块空间创建对象

虽然每个线程都会创建对象, 但是我们的静态指针最后只会拿到最后一个线程创建的对象, 之前创建的都被覆盖

这就造成了内存泄漏, 之前申请的空间没有释放但是丢掉了

非常危险 ! ! !

那么要避免上述情况, 就要让静态指针的判断成为一个原子操作
具体操作就是在判断之前加锁, 之后解锁, 保证操作原子性

这样虽然能解决问题, 但是如果每次来都进行加锁解锁, 这锁也太重了~

严重降低了程序的效率, 所以我们要进行优化

这里采取的措施是在加锁前再进行一次判断

我们先来看一下代码, 之后会详细说明为什么加一个判断

//2. 懒汉模式: 使用的时候再创建
class Singleton2 {
public:
	//静态公有方法, 只有第一次调用时创建对象
	static Singleton2* getobj() {
		//判断标记指针是否为空, 空则创建对象, 不为空就直接返回
		//为了保证线程安全, 需要在判断前加锁, 阻塞其他线程
		//每次加锁时间消耗太大的, 进行优化: 只在第一次创建的时候加锁
		if (_ptr == nullptr) {
			_mtx.lock();
			if (_ptr == nullptr) {
				_ptr = new Singleton2;
			}
			_mtx.unlock();
		}
		return _ptr;
	}
private:
	//构造函数私有化
	Singleton2() {
		cout << "Single()" << endl;
	}

	//防拷贝
	Singleton2(const Singleton2&) = delete;

	//静态指针, 标记空间
	static Singleton2* _ptr;

	//静态锁
	static mutex _mtx;
};
//初始化静态指针和锁
Singleton2* Singleton2::_ptr = nullptr;
mutex Singleton2::_mtx;

下面给大家说明这个加这个 if 能干嘛

两个if判断, 外面的if为了提高效率, 里面的if为了保证单例
第一次创建对象, 此时指针为空, 所有线程都进入第一个if
然后加锁阻塞了其他线程, 只有一个线程进入第二个if, 创建对象
解锁之后其他线程来到第二个if, 此时ptr已经不为空, 其他的线程无法进入if创建对象, 而是直接返回, 这时第一波完成
第二波到来ptr不为空, 第一个if都进不来, 不用加锁解锁, 提高了效率

我们可以看到, 加了一个if判断, 就能让加锁只在线程第一波进入if时加锁解锁, 之后就进不了第一个if, 从而极大的减少了加锁解锁的次数, 极大的提高了代码的性能…


OK, 到这里就结束啦~
大家有问题欢迎评论区提出, 一起学习一起提高 !

你可能感兴趣的:(C++语法/实现/相关,设计模式,c++,多线程)