Singleton
核心:控制某个类型的实例数量是唯一一个。同时为客户程序提供一个访问它的全局访问点。由于类可以被继承,因此Singleton要保证“对外非直接/或间接继承关系”提供一个统一的访问点。
静态的全局变量其实只是提供全局访问的一个技巧,但是不能真正的限制客户端程序实例的数量。说白了,Singleton模式要做的就是通过控制类型实例的创建,确保后续使用都是之前创建好的实例,通过这样的封装,客户程序就无需知道该类型实现的内部细节。
1 | 基础范式 |
经典实现方式:
外部方式
使用者在使用某些全局性(或语义上的全局性)的对象是,做“Try-Create-Store-Use”的工作。
内部方式
类型自己控制生成实例的数量,无论客户程序是否Try过了,类型自己就是一个实例,使用者从头到尾使用的都是这唯一实例。
以上实现方式可以满足最初的设计需求,但是多线程环境下,这种实现方式存在很多的缺陷。但是if语句完成的控制部分在多个县城几乎同时调用Singleton.Instance()时,有可能会被创建多次。最终_intance会被保存为,最后创建的那个实例。
这种方式非常不好复现,因为可能需要靠测试中的运气来触发。为此我们增加“double check”的校验机制
自动更新的Singleton
Singleton从经典实现开始就是一个持续存在的对象,但是实际操作中,持久不变的内容还是放到数据库,等持久化存储中更好一些。内存中的对象还是要被刷新的,工程中的Singleton的唯一实例很多时候也有类似需求。实现上更新触发的原因很多,像游戏中的话,就有可能来自于玩家的热重启,或者切换账户之类的操作。本质上是是一个来自外部的通知。
从模式上可能引发漏洞的地方:
1. 多线程情况下,对于GetInstance()中对于实例的数量控制代码可能会失效。
解决方案1:在原有控制语句的方案上,增加Lock的Double-Check, 并使用volatile关键字。
1 | // voltatile关键字确保字段不受编译器优化,确保该字段在任何时间/// 内都是最新的值 |
解决方案2: 如果本身在构造过程中,不需要借鉴外部机制或者不需要准备很多构造参数,可以使用如下方式来实现线程安全。
1 | class Singleton |
2. 某些情况会导致Singleton模式变质。
场景:比如实现ICloneable接口或即成自相关的子类,否则客户程序可以跳过私有构造函数,让编译器通过内存结构的复制来生成一个新的实例,从而导致并非单一实例存在。或者,通过给Singleton打上SerializableAttribute属性,而在事实上完成了对Singleton对象的拷贝。
解决方案:Don’t do that
Singleton的应用场景:
1: 多线程情况下的共用一个Singleton实例
2: 每个线程都有自己单独的Singleton对象
1 | // 范式: winodws form适用, web form不适用 |
3:暂时不讨论跨进程的Singleton,等遇到了再查阅资料吧。
本质上 Singleton 是属于内存中的对象,并不是持续存在的。所以实际应用中,还是存在被刷新的情况。所以需要留有余地,提供外部/内部的刷新机制。
Singleton的拓展 – Singleton-N:
对外只有一个全局访问接口,实际上类内部有N个实例对象在处理事务。Singleton的批量生产
背景:项目内,我们可能需要使用工厂类型来实现Singleton的实例管理,并通过接口来规范统一的访问点等。
如果按照经典方式定义的Singleton的话,不仅客户程序无法动态生成Singleton实例,外部工厂也生产不了,因为构造函数私有。
可以适当放宽限制,通过工厂实现的Concrete Singleton跟工厂类型定义在同一个程序集中,Singleton的构造函数做到internal可见即可。
总结:
Singleton 更多的强调可构造的类型数量和唯一实力的方式,工程上实现Singleton的挑战主要来自于它的作用空间的同时需要保持Singleton的特质,随着应用的高可用性要求,部署环境中的Singleton需要在更广范围内保持其特性。
理想与现实应用往往有冲突,为了避免过多的重复工作量,稍微放宽Singleton的构造限制有时候是需要纳入考虑的。
