【腾讯Bugly干货分享】那些年,我们一起写过的“单例模式”

2024-03-23 18:48

本文主要是介绍【腾讯Bugly干货分享】那些年,我们一起写过的“单例模式”,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

题记

*度娘上对设计模式(Design pattern)的定义是:“一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。”它由著名的“四人帮”,又称 GOF (即 Gang of Four),在《设计模式》(《Design Patterns: Elements of Reusable Object-Oriented Software》)一书中提升到理论高度,并将之规范化。在我看来,设计模式是前人对一些有共性的问题的优秀解决方案的经验总结,一个设计模式针对一类不断重复发生的问题给出了可复用的、经过了时间考验的较完善的解决方案。使用设计模式可以提高代码的可重用性、可靠性,从而大大提高开发效率,值得我们细细研究。
在这里,我想结合我们的 Android 项目,谈谈大家在其中使用到的一些设计模式。一则,就个人的学习经验看来,研究例子是最容易学会设计模式的方式;二则,其实设计模式的应用同所使用的编程语言和环境都是有关系的,譬如说,我们最先要讨论的单例模式,在 Java 中实现的时候就要特别注意不同 JDK 版本对该模式造成的影响。所以会特意针对我们所关注的 Android 项目进行一些分析。希望通过理论与实践相结合的方式,深入学习设计模式,并自然而然地合理运用到将来,从而完美解决更多问题。*

0 引言

单例模式(Singleton Pattern)一般被认为是最简单、最易理解的设计模式,也因为它的简洁易懂,是项目中最常用、最易被识别出来的模式。既然即使是一个初级的程序员,也会使用单例模式了,为什么我们还要在这里特意地讨论它,并且作为第一个模式来分析呢?事实上在我看来,单例模式是很有“深度”的一个模式,要用好、用对它并不是一件简单的事。

  1. 首先,单例模式可以有多种实现方法,需要根据情况作出正确的选择。
    看名字就知道单例模式的目标就是要确保某个类只产生一个实例,要达到这个目的,代码可以有多种写法,它们各自有不同的优缺点,我们要综合考虑多线程、初始化时机、性能优化、java 版本、类加载器个数等各方面因素,才能做到在合适的情况下选出合用的方法。简单举例看一下 Android 或 Java 中,几个应用了单例模式的场景各自所选择的实现方式:

    isoChronology,LoggingProxy:饿汉模式;
    CalendarAccessControlContext:内部静态类;
    EventBus:双重检查加锁 DCL;
    LayoutInflater:容器方式管理的单例服务之一,通过静态语句块被注册到 Android 应用的服务中。

  2. 其次,单例模式极易被滥用。基本上知道模式的程序员都听说过单例模式,但是在不熟悉的情况下,单例模式往往被用在使用它并不能带来好处的场景下。有很多用了单例的代码并不真的只需要一个实例,这时使用单例模式就会引入不必要的限制和全局状态维护困难等缺陷。通常说来,适合使用单例模式的机会也并不会太多,如果你的某个工程中出现了太多单例,你就应该重新审视一下你的设计,详细确认一下这些场景是否真的都必须要控制实例的个数。

  3. 再者,目前对单例模式也出现了不少争议,使用时更要上心:
    a. 不少人认为,单例既负责实例化类并提供全局访问,又实现了特定的业务逻辑,一定程度上违背了“单一职责原则”,是反模式的。
    b. 单例模式将全局状态(global state)引入了应用,这是单元测试的大敌。
    譬如说 Java 用户都耳熟能详的几个方法:

System.currentTimeMillis();
new Date();
Math.random();

它们是 JVM 中非常常用的暗藏全局状态(global state)的方法,全局状态会引入状态不确定性(state indeterminism),导致微妙的副作用,很容易就会破坏了单元测试的有效性。也就是说多次调用上述的这些方法,输出结果会不相同;同时它们的输出还同代码执行的顺序有关,对于单元测试来说,这简直就是噩梦!要防止状态从一个测试被带到另一个测试,就不能使用静态变量,而单例类通常都会持有至少一个静态变量(唯一的实例),现实中更是静态变量频繁出现的类,从而是测试人员最不想看到的一个模式。
c. 单例导致了类之间的强耦合,扩展性差,违反了面向对象编程的理念。
单例封装了自己实例的创建,不适用于继承和多态,同时创建时一般也不传入参数等,难以用一个模拟对象来进行测试。这都不是健康的代码表现形式。

鉴于上述的这些争议,有部分程序员逐步将单例模式移除出他们的工程,然而这在我看来实在是有点因噎废食,毕竟比起测试的简便性,代码是否健壮易用才是我们的关注点。很多对单例的批评也是基于因为不了解它误用所引发的问题,如果能得到正确的使用,单例也可以发挥出很强的作用。每个模式都有它的优缺点和适用范围,相信大家看过的每一本介绍模式的书籍,都会详细写明某个模式适用于哪些场景。我的观点是,我们要做的是更清楚地了解每一个模式,从而决定在当前的应用场景是否需要使用,以及如何更好地使用这个模式。就像《深入浅出设计模式》里说的:

使用模式最好的方式是:“把模式装进脑子里,然后在你的设计和已有的应用中,寻找何处可以使用它们。”

单例模式是经得起时间考验的模式,只是在错误使用的情况下可能为项目带来额外的风险,因此在使用单例模式之前,我们一定要明确知道自己在做什么,也必须搞清楚为什么要这么做。此文就带大家好好了解一下单例模式,以求在今后的使用中能正确地将它用在利远大于弊的地方,优化我们的代码。

1 单例模式简介

Singleton 模式可以是很简单的,一般的实现只需要一个类就可以完成,甚至都不需要UML图就能解释清楚。在这个唯一的类中,单例模式确保此类仅有一个实例,自行实例化并提供一个访问它的全局公有静态方法。

  • 一般在两种场景下会考虑使用单例(Singleton)模式:

    1. 产生某对象会消耗过多的资源,为避免频繁地创建与销毁对象对资源的浪费。如:

    对数据库的操作、访问 IO、线程池(threadpool)、网络请求等。

  • 某种类型的对象应该有且只有一个。如果制造出多个这样的实例,可能导致:程序行为异常、资源使用过量、结果不一致等问题。如果多人能同时操作一个文件,又不进行版本管理,必然会有的修改被覆盖,所以:
    一个系统只能有:一个窗口管理器或文件系统,计时工具或 ID(序号)生成器,缓存(cache),处理偏好设置和注册表(registry)的对象,日志对象。
  • 单例模式的优点:可以减少系统内存开支,减少系统性能开销,避免对资源的多重占用、同时操作。

  • 单例模式的缺点:扩展很困难,容易引发内存泄露,测试困难,一定程度上违背了单一职责原则,进程被杀时可能有状态不一致问题。

2 单例的各种实现

我们经常看到的单例模式,按加载时机可以分为:饿汉方式和懒汉方式;按实现的方式,有:双重检查加锁,内部类方式和枚举方式等等。另外还有一种通过Map容器来管理单例的方式。它们有的效率很高,有的节省内存,有的实现得简单漂亮,还有的则存在严重缺陷,它们大部分使用的时候都有限制条件。下面我们来分析下各种写法的区别,辨别出哪些是不可行的,哪些是推荐的,最后为大家筛选出几个最值得我们适时应用到项目中的实现方式。

因为下面要讨论的单例写法比较多,筛选过程略长,结论先行:
无论以哪种形式实现单例模式,本质都是使单例类的构造函数对其他类不可见,仅提供获取唯一一个实例的静态方法,必须保证这个获取实例的方法是线程安全的,并防止反序列化、反射、克隆(、多个类加载器、分布式系统)等多种情况下重新生成新的实例对象。至于选择哪种实现方式则取决于项目自身情况,如:是否是复杂的高并发环境、JDK 是哪个版本的、对单例对象资源消耗的要求等。

  • 上表中仅列举那些线程安全的实现方式,永远不要使用线程不安全的单例!
  • 另有使用容器管理单例的方式,属于特殊的应用情况,下文单独讨论。

直观一点,再上一张图:

  • 此四种单例实现方式都是线程安全的,是实现单例时不错的选择
  • 下文会详细给出的三种饿汉模式差别不大,一般使用第二种 static factory 方式

下面就来具体谈一下各种单例实现方式及适用范围。

2.1 线程安全

作为一个单例,我们首先要确保的就是实例的“唯一性”,有很多因素会导致“唯一性”失效,它们包括:多线程、序列化、反射、克隆等,更特殊一点的情况还有:分布式系统、多个类加载器等等。其中,多线程问题最为突出。为了提高应用的工作效率,现如今我们的工程中基本上都会用到多线程;目前使用单线程能轻松完成的任务,日复一日,随着业务逻辑的复杂化、用户数量的递增,也有可能要被升级为多线程处理。所以任何在多线程下不能保证单个实例的单例模式,我都认为应该立即被弃用。

在只考虑一个类加载器的情况下,“饿汉方式”实现的单例(在系统运行起来装载类的时候就进行初始化实例的操作,由 JVM 虚拟机来保证一个类的初始化方法在多线程环境中被正确加锁和同步,所以)是线程安全的,而“懒汉”方式则需要注意了,先来看一种最简单的“懒汉方式”的单例:

这种写法只能在单线程下使用。如果是多线程,可能发生一个线程通过并进入了 if (sing

这篇关于【腾讯Bugly干货分享】那些年,我们一起写过的“单例模式”的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/839202

相关文章

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

【专题】2024飞行汽车技术全景报告合集PDF分享(附原数据表)

原文链接: https://tecdat.cn/?p=37628 6月16日,小鹏汇天旅航者X2在北京大兴国际机场临空经济区完成首飞,这也是小鹏汇天的产品在京津冀地区进行的首次飞行。小鹏汇天方面还表示,公司准备量产,并计划今年四季度开启预售小鹏汇天分体式飞行汽车,探索分体式飞行汽车城际通勤。阅读原文,获取专题报告合集全文,解锁文末271份飞行汽车相关行业研究报告。 据悉,业内人士对飞行汽车行业

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

模版方法模式template method

学习笔记,原文链接 https://refactoringguru.cn/design-patterns/template-method 超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。 上层接口有默认实现的方法和子类需要自己实现的方法

【iOS】MVC模式

MVC模式 MVC模式MVC模式demo MVC模式 MVC模式全称为model(模型)view(视图)controller(控制器),他分为三个不同的层分别负责不同的职责。 View:该层用于存放视图,该层中我们可以对页面及控件进行布局。Model:模型一般都拥有很好的可复用性,在该层中,我们可以统一管理一些数据。Controlller:该层充当一个CPU的功能,即该应用程序

迭代器模式iterator

学习笔记,原文链接 https://refactoringguru.cn/design-patterns/iterator 不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素

《x86汇编语言:从实模式到保护模式》视频来了

《x86汇编语言:从实模式到保护模式》视频来了 很多朋友留言,说我的专栏《x86汇编语言:从实模式到保护模式》写得很详细,还有的朋友希望我能写得更细,最好是覆盖全书的所有章节。 毕竟我不是作者,只有作者的解读才是最权威的。 当初我学习这本书的时候,只能靠自己摸索,网上搜不到什么好资源。 如果你正在学这本书或者汇编语言,那你有福气了。 本书作者李忠老师,以此书为蓝本,录制了全套视频。 试

利用命令模式构建高效的手游后端架构

在现代手游开发中,后端架构的设计对于支持高并发、快速迭代和复杂游戏逻辑至关重要。命令模式作为一种行为设计模式,可以有效地解耦请求的发起者与接收者,提升系统的可维护性和扩展性。本文将深入探讨如何利用命令模式构建一个强大且灵活的手游后端架构。 1. 命令模式的概念与优势 命令模式通过将请求封装为对象,使得请求的发起者和接收者之间的耦合度降低。这种模式的主要优势包括: 解耦请求发起者与处理者

java常用面试题-基础知识分享

什么是Java? Java是一种高级编程语言,旨在提供跨平台的解决方案。它是一种面向对象的语言,具有简单、结构化、可移植、可靠、安全等特点。 Java的主要特点是什么? Java的主要特点包括: 简单性:Java的语法相对简单,易于学习和使用。面向对象:Java是一种完全面向对象的语言,支持封装、继承和多态。跨平台性:Java的程序可以在不同的操作系统上运行,称为"Write once,

springboot实战学习(1)(开发模式与环境)

目录 一、实战学习的引言 (1)前后端的大致学习模块 (2)后端 (3)前端 二、开发模式 一、实战学习的引言 (1)前后端的大致学习模块 (2)后端 Validation:做参数校验Mybatis:做数据库的操作Redis:做缓存Junit:单元测试项目部署:springboot项目部署相关的知识 (3)前端 Vite:Vue项目的脚手架Router:路由Pina:状态管理Eleme