请不要滥用设计模式——SingleTon篇

前言

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

    那么,设计模式是不是随便怎么用都没问题哪?

    这个问题从提问的方式上就可以看出,答案一定是否定的(大家也不是白白接受了这么多年的应试教育的)。

    但是,就我个人的观察,滥用设计模式的绝对不是少数。而且越是简单的模式越会被滥用。

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

    说到SingleTon,我相信只要知道设计模式的,就知道SingleTon,也写过SingleTon,可谓是尽人皆知的设计模式了。

    就是这个尽人皆知的设计模式,却是被滥用的最厉害的设计模式,本篇就讨论一下关于SingleTon的滥用问题。

线程安全是个问题

    首先GoF是站在一个纯OO的领域思考问题的,所以,很多其他领域的问题并没有考虑进来(事实上也不适合拉进来一起讲),然而实际编程者却不得不面对更多领域的问题(最常见的是并发领域)。

    这也就是为什么在GoF的SingleTon是如此的简单,而在Java或.net实际写SingleTon时,却需要注意锁的问题的根本原因。

    关于SingleTon是否是线程安全的,我倾向于把问题分解成两个部分:

  • SingleTon本身是否线程安全
  • 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<Person>
{
    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<Person>接口的实例,因此不能使用静态方法(假设不能使用委托)。

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

后话

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

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

    哎,SingleTon啊SingleTon,真是GoF引入OO的最大的坑。

原文地址:https://www.cnblogs.com/vwxyzh/p/1779620.html