文章目录
- 单例模式
- 什么叫做单例模式
- 单例模式的动机
- 单例模式的引入
- 思考
- 饿汉式单例和懒汉式单例
- 饿汉式单例
- 懒汉式单例
- 单例模式总结
- 1.主要优点
- 2.主要缺点
- 3.适用场景
单例模式
什么叫做单例模式
顾名思义,简单来说,单例模式就是只有一个用例的意思
那么官方一点解释是什么:
单例模式是一种设计模式,它确保某个类只有一个实例,并提供一个全局访问点来获取该实例。这种模式在多种编程语言中都有实现,包扩Java和C++。单例模式的实现可以采取饿汉式或懒汉式两种方式。**饿汉式是在类加载时就创建了实例,而懒汉式则是在首次使用时才创建实例。**懒汉式在多线程环境下可能会遇到线程安全问题,需要额外的线程安全措施来保证单例对象的唯一性
单例模式的动机
这里有一个问题:Windows的任务管理器无论启动多少次,为什么始终只能弹出一个任务管理器的窗口呢?
也就是说在一个Windows系统中,任务管理器存在唯一性,为什么要这样设计呢?
可以从以下两个方面来分析:
- 其一,如果能弹出多个窗口,且这些窗口的内容完全一致,全部是重复对象,这势必会浪费系统资源(任务管理器需要获取系统运行时的诸多信息,这些信息的获取需要消耗一定的系统资源,包括CPU资源及内存资源等),而且根本没有必要显示多个内容完全相同的窗口;
- 其二,如果弹出的多个窗口内容不一致,问题就更加严重了,这意味着在某一瞬间系统资源使用情况和进程、服务等信息存在多个状态,例如任务管理器窗口A显示“CPU使用率”为10%,窗口B显示“CPU使用率”为15%,到底哪个才是真实的呢?这会给用户带来误解,更不可取。
在实际开发中也经常遇到类似的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,可以通过单例模式来实现,这就是单例模式的动机所在。
单例模式的引入
模拟实现一下Windows任务管理器
为了实现WIndows任务管理器的唯一性,通过以下三步对TaskManager类进行重构
-
由于每次使用new关键字来实例化TaskManager类时都将产生一个新对象,为了确保TaskManager实例的唯一性,需要禁止类的外部直接使用new来创建对象,因此需要将TaskManager的构造函数的可见性改为private,代码如下:
-
将构造函数的可见性改为private后,虽然类的外部不能再使用new来创建对象,但是在TaskManager的内部还是可以创建对象的,可见性只对类外有效。因此,可以在TaskManager中创建并保存这个唯一实例。为了让外界可以访问这个唯一实例,需要在TaskManager中定义一个静态的TaskManager类型的私有成员变量,代码如下:
-
为了保证成员变量的封装性,将TaskManager类型的tm对象的可见性设置为private,但外界该如何使用该成员变量并何时实例化该成员变量呢?答案是增加一个公有的静态方法,代码如下:
在getInstance()方法中首先判断tm对象是否存在,如果不存在(即tm==null为true),则使用new关键字创建一个新的TaskManager类型的tm对象,再返回新创建的tm对象;否则直接返回已有的tm对象。
需要注意的是getInstance()方法的修饰符,首先它应该是一个public方法,以便外界其他对象使用;其次它使用了static关键字,即它是一个静态方法,在类外可以直接通过类名来访问,而无须创建TaskManager对象。事实上,在类外也无法创建TaskManager对象,因为构造函数是私有的。
思考
为什么要将成员变量tm定义为静态变量?
通过以上三个步骤,完成了一个个最简单的单例类的设计,其完整代码如下:
class TaskManager{
private:
TaskManager(){}//初始化窗口
public:
void displayProcess(){}//显示进程
void displayServices(){}//显示服务
public:
static TaskManager* getInstance(){
if(tm == nullptr)
{
tm = new TaskManager();
}
return tm;
}
private:
static TaskManager *tm;
};
TaskManager * TaskManager::tm = nullptr;
在类外无法直接创建新的TaskManager对象,但可以通过代码TaskManager::getInstance()
访问实例对象。第一次调用getInstance()方法时将创建唯一实例,再次调用时将返回第一次创建的实例。
饿汉式单例和懒汉式单例
上面的简单的单例模式存在一个问题,在多线程的场景下,当第一次调用getInstanc()时创建并对象时,tm为nullptr值,因此系统将执行代码 tm = new Taskanager();
在这个过程中,如果初始化工作要进行大量工作,则需要一段时间来创建TaskManager对象。而在此时,如果再一次调用getInstance()方法(通常发生在多线程环境中),由于TaskManager对象尚未创建成功,仍为nullptr值,判断条件"tm == nullptr"为真值,因此代码 tm = new TaskManager();
会再次执行,导致最终创建了多个tm对象,这违背了单例模式的初衷,也可能会导致发生运行错误。
那么如何解决这个问题呢?
饿汉式单例
从图中可以看到当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建,可确保单例对象的唯一性。
懒汉式单例
在前面的单例模式的引入介绍中,我们用的就是懒汉式的单例模式,但是在多线程的场景中会出现问题,在 C++11 中,静态局部变量这种方式天然是线程安全的,不存在线程不安全的问题。原因是C++ 11标准中新增了一个特性叫Magic Static:如果变量在初始化时,并发线程同时进入到static声明语句,并发线程会阻塞等待初始化结束。
但是为了方便学习我们还是需要在这里引入互斥锁
class LazySingleton
{
private:
LazySingleton() {}
~LazySingleton() {}
private:
static LazySingleton *instance;
static std::mutex mutex; // 互斥锁
public:
static LazySingleton *getInstance()
{
if (instance == nullptr)
{
std::lock_guard<std::mutex> lock(mutex); // 加锁
if (instance == nullptr)
{
instance = new LazySingleton();
}
}
return instance;
}
};
LazySingleton* LazySingleton::instance = nullptr;
单例模式总结
单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。
1.主要优点
单例模式的主要优点如下:
(1)单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
(2)由于在系统内存中只存在一个对象,因此可以节约系统资源。对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
(3)允许可变数目的实例。基于单例模式,开发人员可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题。(注:自行提供指定数目实例对象的类可称之为多例类。)
2.主要缺点
单例模式的主要缺点如下:
(1)由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
(2)单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。
(3)现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
3.适用场景
在以下情况下可以考虑使用单例模式:
(1)系统只需要一个实例对象。例如,系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
(2)客户调用类的单个实例只允许使用一个公共访问点。除了该公共访问点,不能通过其他途径访问该实例。
——————————————————————————————————————————————————————————————————————————————————————————————————————————————
📜 参考资料
设计模式的艺术—— 刘伟—— 清华大学出版社 ->链接