【腾讯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

相关文章

Python中常用的四种取整方式分享

《Python中常用的四种取整方式分享》在数据处理和数值计算中,取整操作是非常常见的需求,Python提供了多种取整方式,本文为大家整理了四种常用的方法,希望对大家有所帮助... 目录引言向零取整(Truncate)向下取整(Floor)向上取整(Ceil)四舍五入(Round)四种取整方式的对比综合示例应

linux进程D状态的解决思路分享

《linux进程D状态的解决思路分享》在Linux系统中,进程在内核模式下等待I/O完成时会进入不间断睡眠状态(D状态),这种状态下,进程无法通过普通方式被杀死,本文通过实验模拟了这种状态,并分析了如... 目录1. 问题描述2. 问题分析3. 实验模拟3.1 使用losetup创建一个卷作为pv的磁盘3.

MySQL8.2.0安装教程分享

《MySQL8.2.0安装教程分享》这篇文章详细介绍了如何在Windows系统上安装MySQL数据库软件,包括下载、安装、配置和设置环境变量的步骤... 目录mysql的安装图文1.python访问网址2javascript.点击3.进入Downloads向下滑动4.选择Community Server5.

Java实现状态模式的示例代码

《Java实现状态模式的示例代码》状态模式是一种行为型设计模式,允许对象根据其内部状态改变行为,本文主要介绍了Java实现状态模式的示例代码,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来... 目录一、简介1、定义2、状态模式的结构二、Java实现案例1、电灯开关状态案例2、番茄工作法状态案例

CentOS系统Maven安装教程分享

《CentOS系统Maven安装教程分享》本文介绍了如何在CentOS系统中安装Maven,并提供了一个简单的实际应用案例,安装Maven需要先安装Java和设置环境变量,Maven可以自动管理项目的... 目录准备工作下载并安装Maven常见问题及解决方法实际应用案例总结Maven是一个流行的项目管理工具

10个Python自动化办公的脚本分享

《10个Python自动化办公的脚本分享》在日常办公中,我们常常会被繁琐、重复的任务占据大量时间,本文为大家分享了10个实用的Python自动化办公案例及源码,希望对大家有所帮助... 目录1. 批量处理 Excel 文件2. 自动发送邮件3. 批量重命名文件4. 数据清洗5. 生成 PPT6. 自动化测试

10个Python Excel自动化脚本分享

《10个PythonExcel自动化脚本分享》在数据处理和分析的过程中,Excel文件是我们日常工作中常见的格式,本文将分享10个实用的Excel自动化脚本,希望可以帮助大家更轻松地掌握这些技能... 目录1. Excel单元格批量填充2. 设置行高与列宽3. 根据条件删除行4. 创建新的Excel工作表5

Redis多种内存淘汰策略及配置技巧分享

《Redis多种内存淘汰策略及配置技巧分享》本文介绍了Redis内存满时的淘汰机制,包括内存淘汰机制的概念,Redis提供的8种淘汰策略(如noeviction、volatile-lru等)及其适用场... 目录前言一、什么是 Redis 的内存淘汰机制?二、Redis 内存淘汰策略1. pythonnoe

Golang操作DuckDB实战案例分享

《Golang操作DuckDB实战案例分享》DuckDB是一个嵌入式SQL数据库引擎,它与众所周知的SQLite非常相似,但它是为olap风格的工作负载设计的,DuckDB支持各种数据类型和SQL特性... 目录DuckDB的主要优点环境准备初始化表和数据查询单行或多行错误处理和事务完整代码最后总结Duck

将Python应用部署到生产环境的小技巧分享

《将Python应用部署到生产环境的小技巧分享》文章主要讲述了在将Python应用程序部署到生产环境之前,需要进行的准备工作和最佳实践,包括心态调整、代码审查、测试覆盖率提升、配置文件优化、日志记录完... 目录部署前夜:从开发到生产的心理准备与检查清单环境搭建:打造稳固的应用运行平台自动化流水线:让部署像