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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值