单例模式——创建型模式(5)
前言
单例模式是我们所要介绍的创建型模式中的最后一种设计模式,它与我们前面介绍过的四种创建型模式有相似之处,亦有很大的不同之处。相似之处是它们都属于创建型模式,抽象了对象类实例化的过程;而不同之处是在于单例模式在创建对象实例时,在全局范围内保证只会创建存在该对象类的一个实例对象,同时提供其全局访问点,而其他的四个创建型模式并没有此限制,可以自由地创建实例化多个对象类实例,这是它们之间的最大区别。单例模式在特定的场合下,有其独到的用处,接下来,就让我们揭开其神秘的面纱吧!
动机
在软件系统中,有时会出现这样一种需求:在系统运行时刻,某一个类在全局范围内只有一个对象实例,而且获取该对象实例只能通过该对象类特定的访问接口来获取,以便绕过常规的构造器,来避免在全局运行环境中实例化多个类对象实例。面对这样的一种需求,如何提供一种封装机制来保证一个对象类只有一个实例?需要注意的是,客户端使用该对象类时,是不会考虑此类是否只有一个实例存在的问题,这应该属于类设计者的责任,由其来保证,而不是类使用者的责任。同时,由于这个全局唯一的对象实例拥有了所属类的全部“权力”,自然它也就担负起了行使这些权力的职责!这就是我们说的——单例模式!
意图
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
结构图
- Singleton(单例)角色:定义一个Instance操作,允许客户访问它的唯一实例。Instance是一个类操作,通常为静态方法,负责创建它自己的唯一实例。
代码示例
1: public class Singleton {
2: static Singleton instance=null;
3: private Singleton(){
4:
5: }
6: public static Singleton Instance(){
7: if(instance==null){
8: instance=new Singleton();
9: }
10: return instance;
11: }
12: //省略其他功能方法。。。
13: }
上述示例代码是单例模式最简单的实现,实现了单例模式的基本要求,提供一个全局访问点来获取得到单例类对象唯一对象实例,同时将单例类的构造函数访问修饰符设置为private,阻止客户程序直接通过new的方式来创建单例类的对象实例,保证对象的全局唯一性。当然,这只是示例代码,在实际的生产环境中,我们需要根据线程安全、性能等方面来改造单例模式的实现方式,下方将会有详细的讲解。
现实场景
在我们日常的现实生活中,有很多常见的场景与我们所讲的单例模式很相似。比如,每台计算机可以有很多打印机,但是只能有一个Printer Spooler,避免两个打印任务同时输出到打印机中;国家**职位是一个单例模式,因为我们国家宪法规定在任何一个时刻,国家只能有一个国家**(不*括副**),所以不管当前**个人身份如何,我们总是可以通过中华人民共和国**这个职称来获取当前国家**的所有信息,换句话来说,也只有通过它来行使宪法赋予它的各种权力和义务,因为它的”全局唯一性“。再比如,就是我们日常web开发常用的spring开源框架中对各种bean创建时的对象实例个数的指定,即scope=”singleton”,这个配置项的目的便是告之spring容器,该bean在运行时刻只能存在一个全局实例,也就是任何一个地方引用的都是这个唯一的实例对象,深入spring源码,我们也不能发现,其实其内部基本上也是单例模式的实现而已。
接下来,我们着重来讲述一下,对单例模式的不同实现方式及其特点吧。这也是单例模式中最有意义的部分呢,希望大家一起来学习、理解并掌握它们的不同之处。
示例代码就是一种最简单的单例实现方式,在单线程的环境之下,基本可以胜任,但是若是置于多线程的环境中,就面临着线程安全问题呢。之所以这么说,是因为可能会出现多个单例对象。下面我们通过图示的方法来说明一下示例代码中获取单例对象的Instance()方法是如何在运行时刻创建出两个所谓的单例对象的,现在假设,有两个对线程A和B,它们同时调用Instance()方法,那么实际的执行过程就有可能是这样的:
如果按照上图的执行顺序,那么,这里A线程和B线程就都各创建了一个单例实例对象,也就违反了单例模式的本质。示例代码中的单例模式实现方式为懒汉式,如果要求线程安全,通常有两种方式,一个是在Instance()方法上加上sysnchronized关键字,即:
1: public static synchronized Singleton Instance(){
2: if(instance==null){
3: instance=new Singleton();
4: }
5: return instance;
6: }
关键字synchronized将会保证Instance()方法的线程安全,但是这样一来,会降低整个访问速度,而且每次都需要进行判断。有没有一种更好的方式来实现懒汉式实现方式即线程安全又能保证执行效率呢?
答案就是双重加锁机制,具体指的是:并不是每次进入Instance()方法都需要进行同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检票。进入同步块后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个单例对象实例,这是第二重检查。这样一来,就只需要同步一次,从而减小了同步情况下进行判断所浪费的时间。
双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。需要注意的是,在java1.4版本及之前版本中,很多JVM对volatile关键字的实现有问题,建议在java5及以上版本上使用。实现代码如下:
1: public class Singleton {
2: private static volatile Singleton instance=null;
3: private Singleton(){
4:
5: }
6: public static Singleton Instance(){
7: if(instance==null){
8: synchronized (Singleton.class) {
9: if(instance==null){
10: instance=new Singleton();
11: }
12: }
13: }
14: return instance;
15: }
16: }
这里需要提及的一点是,由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不建议使用。换句话来说,虽然使用双重加锁机制可以实现线程安全的单例模式,但并不建议大量使用。既然这样子,是否有一种更加合理的方式来完成对单例模式的实现呢?
这里也有两种选择,一种是饿汉式实现方式,一种还是懒汉式实现方式。饿汉式实现方式比较简单,直接在Singleton类加载的过程中,就将静态类型的instance变量实例化,由虚拟来保证其线程安全性,唯一不足的是无法实现延迟加载,不过这种方式,通过情况下已经足够高效呢,实现也比较简单,示例代码如下所示:
1: public class Singleton {
2: static Singleton instance=new Singleton();
3: private Singleton(){}
4:
5: public static Singleton Instance(){
6: return instance;
7: }
8: }
不过也有一种懒汉式的实现方方式,名日:Lazy initialization holder class模式,其综合使用了java的类级内部类和多线程缺省同步锁知识,很巧妙地同时实现了延迟加载和线程安全,在介绍其具体的实现之前,我们先来普及下基础知识吧!
何谓类级内部类,指的是有static修饰的成员式内部类,如果没有static修饰的内部类称为对象级内部类。类级内部类相当于其外部类的static成分,它与对象与外部类对象间不存在依赖关系,因此可以直接创建。在类级内部类中,可以定义静态的方法,在静态方法中只能引用外部类中的静态方法或者静态成员变量。类级内部类相当于外部类的成员,只有在第一次被使用时才会被加载。
接下来,我们看看有哪些情况下,JVM会隐含地为我们执行同步操作,这些情况下,我们不需要自己来进行同步控制呢:
- 由静态初始化器(在静态字段上或者是static{}块中的初始化器)初始化数据时。
- 访问final字段时。
- 在创建线程之前创建对象时。
- 线程可以看见它将要处理的对象时。
有了上面两部分知识,再来理解这种高效的单例实现方式就比较简单呢!
1: public class Singleton {
2: //类级内部类,与外部类实例没有绑定关系,只有在被调用时才会被加载,从而实现了延迟加载
3: private static class SingletonHolder{
4: //静态初始化器,由JVM来保证线程安全
5: private static Singleton instance=new Singleton();
6: }
7:
8: private Singleton(){}
9:
10: public static Singleton Instance(){
11: return SingletonHolder.instance;
12: }
13: }
结合上面的基础知识和代码上的注释,仔细想想,这种方法是不是很巧妙呢?在Instance()方法第一次被调用时,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而SingletonHolder类被装载并初始化的时候,也会初始化其静态域,也就会创建Singleton的对象实例,因为是静态域,因此会由在虚拟机装载类的时候初始化一次,并由JVM来保证其线程安全性。综上所述,该实现方法,不仅实现了延迟加载,又实现了线程安全,确实是一种值得推荐的单例实现方法,大家好好理解此方法的精妙之处吧!(来自《研磨设计模式》一书)
实现要点
- 单例模式模式用于限制对单例类实例的创建
- Singleton类的构造器可以是protected,被子类派生
- Singleton类一般不需要实现Cloneable接口,因为克隆操作可能会产生多个单例类对象,与单例模式的初衷相佐
- 单例模式关注点在单例类的实例的创建上,没有涉及实例的销毁管理等工作,我们可以通过使用一个单例注册表来完成对各种单例类实例的管理工作。
运用效果
- 对象实例控制:单例模式提供全局访问点,可以保证对客户端都只访问到唯一一个单例实例对象。
- 创建的方便性:由于单例类控制了实例创建,可以根据实际情况方便地修改单例对象的实例化过程。
- 由于单例模式不能通过new的方式直接创建单例对象,因此单例类的使用都必须事先知道该类为单例类,否则会因为看不到源码,而造成类的使用性差的印象。
- 由于单例类全局只有一个对象实例,但是对其的引用却可能不只一个,因为不能简单地对这个特殊的对象实例进行销毁操作,换句话来说就是不能轻易地手工地销毁该对象。在存在内存管理的语言中,这个问题我们可以不能过多地关注这个问题,运行时会帮我们自动销毁已经不存在引用的对象,但是对于c++语言而言,如果简单地销毁单例对象,有可能会造成“悬浮引用”问题。
适用性
- 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。
- 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时(这点不是很理解,理解的同学留言欢迎交流)。
相关模式
很多模式都可以使用单例模式,只要这些模式的某个类,需要控制实例为一个的时候,就可以通过单例模式来完成。比如抽象工作方法中的具体工厂类通常就可以设计成一个单例类等等。
总结
单例模式的本质是:控制实例数目。这里说的实例数目不一定就指的是一个单例实例,只是说是实例对象数量有一定限制而已。通过单例模式我们可以很容易控制单例类的创建和访问,请记住,单例类实例的个数控制问题是类设计者应该考虑的问题,而不是类使用者考虑的问题,作为单例类的设计者,我们应该时刻记住这一原则,指导我们实现单例模式。单例模式的几种通常的实现方法在上文中已经有比较详细的介绍,希望大家能够好好理解其中内含,学以致用。下一篇,我们将会对之前介绍过的所有创建型模式进行概括性地梳理和总结,敬请期待!
参考资料:
- 程杰著《大话设计模式》一书
- 陈臣等著《研磨设计模式》一书
- GOF著《设计模式》一书
- Terrylee .Net设计模式系列文章
- 吕震宇老师 设计模式系列文章