热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

单例模式虽好但请不要滥用

说到SingleTon,我相信只要知道设计模式的,就知道SingleTon,也写过SingleTon,可谓是尽人皆知的设计模式了。就是这个尽人皆知的设计模式,却是被滥用的最厉害的设计模式,本篇就讨论一下关于SingleTon的滥用问题。首先GoF是站在一个纯OO的领域思考问题的,所以,很多其他领域的问题并没有考虑进来,

说到面向对象的设计模式,现在很多人都可以随便说出好几种常用的,但是有没有想过设计模式,即使是初学者也至少能说一下SingleTon和Factory Method这两个。那么,设计模式是不是随便怎么用都没问题哪?

这个问题从提问的方式上就可以看出,答案一定是否定的(大家也不是白白接受了这么多年的应试教育的)。 但是,就我个人的观察,滥用设计模式的绝对不是少数。而且越是简单的模式越会被滥用。

从最简单的模式——SingleTon开始。

说到SingleTon,我相信只要知道设计模式的,就知道SingleTon,也写过SingleTon,可谓是尽人皆知的设计模式了。就是这个尽人皆知的设计模式,却是被滥用的最厉害的设计模式,本篇就讨论一下关于SingleTon的滥用问题。

线程安全是个问题

首先GoF是站在一个纯OO的领域思考问题的,所以,很多其他领域的问题并没有考虑进来(事实上也不适合拉进来一起讲),然而实际编程者却不得不面对更多领域的问题(最常见的是并发领域)。这也就是为什么在GoF的SingleTon是如此的简单,而在Java或.net实际写SingleTon时,却需要注意锁的问题的根本原因。关于SingleTon是否是线程安全的,我倾向于把问题分解成两个部分:

  1. SingleTon本身是否线程安全
  2. SingleTon的实例是否线程安全

关于第一个,通过著名的Java下双检锁不安全问题,相信大家都已经十分清楚了,如果还有不清楚的,请查阅相关资料,本文就不再重复了。在这里,要重点说的是第二个问题,SingleTon的实例是否线程安全。

如果仔细看msdn的话,在大多数类上,msdn都写了这么一句话:Public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.

在msdn中文版中是:此类型的公共静态(在 Visual Basic 中为 Shared)成员是线程安全的。但不能保证任何实例成员是线程安全的。

在ms给出的类库,把静态成员都写成线程安全的,而对待大部分的实例成员却是放任其线程的不安全,而要求开发人员在开发时处理实例成员的线程不安全问题(我相信Java的类库也是类似的做法)。

那么在多线程程序中使用SingleTon,会引出什么问题哪?首先,SingleTon仅仅允许一个类只有一个实例。那么这里简单的做个推理:

  1. 在这个程序里面无论何时,当你需要这个类的实例的时候,都只能使用这个唯一的实例。
  2. 无论在这个程序的哪个线程中,当你需要这个类的实例的时候,都只能使用这个唯一的实例。
  3. 无论在这个程序中出现什么样的并发,当你需要这个类的实例的时候,都只能使用这个唯一的实例。
  4. 无论在这个程序中出现什么样的并发,当你需要调用这个类的实例的某个成员时,都只能使用这个唯一的实例的成员。
  5. 无论在这个程序中出现什么样的并发,为了保证程序是线程安全的,当你需要调用这个类的实例的某个成员时,都只能使用这个唯一的实例的成员,并且需要保证其线程安全。
  6. 无论在这个程序中出现什么样的并发,为了保证程序是线程安全的,当你需要调用这个类的实例的某个成员时,如果这个成员不是线程安全的,那么在使用这个唯一的实例的成员时都需要正确的线程同步。

发现问题了没有,被SingleTon的实例成员的线程安全性需要谁来保证?

  • 选项A:SingleTon的实例保证所有的成员是线程安全的
  • 选项B:SingleTon的实例不保证线程安全的,请大家在使用的时候都加上锁

如果选择A,那么,请检查那些被SingleTon的代码,看看有没有用到堆,检查对堆的任何调用是否都是线程安全的,或者已经同步的。

如果选择B,那么,请检查所有使用SingleTon的代码,看看是否都经过了可靠的线程同步(例如Lock那个SingleTon的对象),只要有一处不注意,就导致整个是线程不安全的(但是当下这么多写网页的人,有多少人会去关注那些资源是需要线程同步的吗?)。

有没有选项C?有,如果是类库的话,对外宣称程序是线程不安全的,请在使用前保证线程安全(例如COM中著名的STA);如果是应用程序的话就告诉客户,本程序是线程不安全的,出任何问题都是有可能的,当初合同就没说要保证线程安全(客户一定会抓狂)。

为什么要用SingleTon?

不知道大家有没有想过,当初为什么要把这个类型用SingleTon来做。我看到的大多数答案是节省资源和全局状态,固定算法的接口适配(不排除还有更好的答案)。

先讨论节省资源的问题,首先不new实例一定比new实例要节省资源(CPU和内存),这点不用质疑,但是如果要做到线程安全,似乎就要再考量一下了。

如果SingleTon实例本身的实现方式就保证了线程安全(仅使用堆栈和参数中的对象,对自身引用的对象只读),那么线程安全是0代价的。如果其实现方式涉及Lock等同步,那么冲突概率是多少,如果冲突概率足够的高,那么大多数时间线程将进入等待状态,导致大量占用时间资源和CPU资源。即使冲突概率很低,由于Lock需要同步Cache和内存,所以一样需要花费一些额外的代价(同步Cache的时间代价)。

说到这里,还觉得有Lock的SingleTon一定比new实例节省资源?未必吧,到底谁省资源还是要具体问题具体分析一下,不Profiling一下,谁又知道结果哪?

至于全局状态,可以的话,要尽量避免全局状态的使用,如果必须要使用的话,确实没什么好的方案,不过,建议作为全局状态使用的SingleTon需要保证所有成员的线程安全(否则,一个team member的小错误,就可能导致全局状态出错)。

第三种固定算法的接口适配,这是我比较提倡的SingleTon用法。这里涉及几个部分的要求:

  1. 算法固定
  2. 算法不存在状态,所有的变化均来自参数,且实现无需线程同步就可以保证线程安全
  3. 接收方要求符合某接口(泛指)

如果第一点不满足,那就不可能作为SingleTon存在。第二点是确保使用SingleTon的性能优势。而第三点是OO立场上的SingleTon必要性,否则为什么不用静态方法。

也许这3点比较抽象,举个实际点的例子:

public class PersonNameComparer
    : IComparer
{
    public int Compare(Person x, Person y)
    {
        if (x == y)
            return 0;
        if (x == null)
            return -1;
        if (y == null)
            return 1;
        return string.Compare(x.name, y.name);
    }
}

第一,算法固定,就是比较2个人的名字,没有第二种算法,要是有那也是其它类的职责;第二,不存在状态,只用了x和y两个参数,没有线程同步;第三,因为需要在排序的场合需要IComparer接口的实例,因此不能使用静态方法(假设不能使用委托)。

因此,这个类型如果是SingleTon的话,那将是合理的(当然这里不用SingleTon也没问题)。

后话

N年前,我去面试某公司时,某面试官问我数据库联接能不能做SingleTon,我说不能,会引入很多问题,结果面试官很不满意。

N月前,面试某人,在谈到设计模式用在什么地方时候,举例说在Web项目中把一个WebService的代理类做成了SingleTon,结果我很不满意。哎,SingleTon啊SingleTon,真是GoF引入OO的最大的坑。

本文地址:http://www.nowamagic.net/librarys/veda/detail/921,欢迎访问原出处。


推荐阅读
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 深入解析JVM垃圾收集器
    本文基于《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版,详细探讨了JVM中不同类型的垃圾收集器及其工作原理。通过介绍各种垃圾收集器的特性和应用场景,帮助读者更好地理解和优化JVM内存管理。 ... [详细]
  • 非公版RTX 3080显卡的革新与亮点
    本文深入探讨了图形显卡的进化历程,重点介绍了非公版RTX 3080显卡的技术特点和创新设计。 ... [详细]
  • Docker的安全基准
    nsitionalENhttp:www.w3.orgTRxhtml1DTDxhtml1-transitional.dtd ... [详细]
  • Valve 发布 Steam Deck 的新版 Windows 驱动程序
    Valve 最新发布了针对 Steam Deck 掌机的 Windows 驱动程序,旨在提升其在 Windows 环境下的兼容性、安全性和性能表现。 ... [详细]
  • 计算机网络复习:第五章 网络层控制平面
    本文探讨了网络层的控制平面,包括转发和路由选择的基本原理。转发在数据平面上实现,通过配置路由器中的转发表完成;而路由选择则在控制平面上进行,涉及路由器中路由表的配置与更新。此外,文章还介绍了ICMP协议、两种控制平面的实现方法、路由选择算法及其分类等内容。 ... [详细]
  • 本文将介绍如何使用 Go 语言编写和运行一个简单的“Hello, World!”程序。内容涵盖开发环境配置、代码结构解析及执行步骤。 ... [详细]
  • 1:有如下一段程序:packagea.b.c;publicclassTest{privatestaticinti0;publicintgetNext(){return ... [详细]
  • 本文详细介绍了如何在Linux系统上安装和配置Smokeping,以实现对网络链路质量的实时监控。通过详细的步骤和必要的依赖包安装,确保用户能够顺利完成部署并优化其网络性能监控。 ... [详细]
  • 本文介绍如何利用动态规划算法解决经典的0-1背包问题。通过具体实例和代码实现,详细解释了在给定容量的背包中选择若干物品以最大化总价值的过程。 ... [详细]
  • 数据管理权威指南:《DAMA-DMBOK2 数据管理知识体系》
    本书提供了全面的数据管理职能、术语和最佳实践方法的标准行业解释,构建了数据管理的总体框架,为数据管理的发展奠定了坚实的理论基础。适合各类数据管理专业人士和相关领域的从业人员。 ... [详细]
  • 题目描述:给定n个半开区间[a, b),要求使用两个互不重叠的记录器,求最多可以记录多少个区间。解决方案采用贪心算法,通过排序和遍历实现最优解。 ... [详细]
  • 深入理解C++中的KMP算法:高效字符串匹配的利器
    本文详细介绍C++中实现KMP算法的方法,探讨其在字符串匹配问题上的优势。通过对比暴力匹配(BF)算法,展示KMP算法如何利用前缀表优化匹配过程,显著提升效率。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • 深入解析:手把手教你构建决策树算法
    本文详细介绍了机器学习中广泛应用的决策树算法,通过天气数据集的实例演示了ID3和CART算法的手动推导过程。文章长度约2000字,建议阅读时间5分钟。 ... [详细]
author-avatar
朱鹏飞0521
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有