创办模式之单例、原型

创建模式之单例、原型

      在创建型模式中,单例(Singleton)模式和原型(Prototype)模式相对来说其用意更为简单明了。单例(Singleton)模式确保某类只有一个实例,且自行实例化并向整个系统提供这个实例;原型(Prototype)模式通过给出一个原型对象来指明所要创建的对象类型,并通过Clone的方式创建出所需的同类型的对象。接下来,我们针对这两种模式的一些常见问题给出简单阐述。

      #单例(Singleton)模式

            #特点:1、只能有一个实例;2、必须自行创建这个实例;3、必须自行向整个系统提供这个实例;4、因构造方法似有,不可被继承。其中,1是通过构造方法来保障的,2&3是通过工厂方法来实现的。
            #使用单例的必要条件(使用场景):一个系统要求一个类只有一个实例时。

            #实现方式及其存在特点:在上篇博文我们曾提到过,单例(Singleton)模式常被拿来作为笔试考察的,侧重于单例模式的实现(所谓的饿汉、懒汉、注册、双重检查等实现方式优劣以及多类加载器、多JVM情况下Singleton的表现)及其使用场景。本篇博文将着力解决这几个问题。

             #实现方式之饿汉:java语言里实现起来最为简便的单例(Singleton),下面是其示意图及其实现代码。

创办模式之单例、原型

             #实现方式之懒汉:与饿汉相比,相同之处为构造方法均为私有(类不能被继承),不同之处为饿汉在加载时被实例化,采用了静态工厂方法提供自身唯一的实例;而懒汉是在第一次调用时被实例化,采用了同步化的静态工厂方法提供自身唯一的实例。下面是其示意图及其实现代码。

创办模式之单例、原型

            #饿汉与懒汉特点:其一私有构造子使其均无法被继承,其二加载方式带来的在资源利用效率与反应速度上的差异性。饿汉式单例在自己加载时就将自己实例化,即便加载器是静态的。仅从资源利用效率角度来讲,这个比懒汉式单例稍差点,但从时间的反应速度角度来讲,则比懒汉式单例稍好些。懒汉式单例在实例化时,必须处理好在多个线程同时首次引用此类时的访问限制问题,特别是当单例类作为资源控制器在实例化时必然涉及资源初始化,而资源初始化可能耗费时间,这意味着出现多线程同时首次引用此类的几率变得较大。

            注:饿汉单例类可以在java语言内实现,但不易在C++内实现,因为静态初始化在C++中没有固定顺序,因而静态变量的初始化与类的加载顺序没有保证,可能会出问题。这便是为何GOF在提出单例概念时,举得例子是懒汉式的。因其影响力之大,以致java语言中单例类的例子大多是懒汉式。实际上,对于java,推荐饿汉式。

            #实现方式之注册:是GOF为了克服以上两种方式实现的单例均无法继承的缺点而设计的。顺便提下,一般情况下这种方式并不是十分必要的,它只是提供了一种单例需求的实现方式,但不能保证单例,因为它开放了构造方法的保护等级,在这里仅做一般了解即可。不过,这个模式却给我了一个多例的实现思想,所谓多例,即允许n(n>1)个实例而不单单一个实例,但我们平常用到较多的类似于多例的是池的概念,两者的不同之处在于池强调可用对象的量,而多例在于强调量与构造方法的私有。下面示意图的代码实现上,基类采用了饿汉方式,但其子类的实例化只能是懒汉式的,这是无法改变的。

创办模式之单例、原型

             #实现方式之双重检查成例(java中不可实现)

                   我们说过,在上面给出的懒汉式单例实现里对静态工厂方法使用了同步化,以处理多线程环境。有些设计师建议这里使用所谓的“双重检查成例”,之所以提到它,是因为它在C语言中得到了普遍的应用,然而,需要指出的是,这是不可以在java中使用的。我们首先来看一下,java中“双重检查成例”单例的实现:

package pattern.creational.singleton;
/**
*这是一个反面的错误例子
*/
public class LasySingleton {
   private static LasySingleton instance = null;

   private LasySingleton() {}

   public static LasySingleton getInstance() {
       if(instance == null) {
           synchronized(LasySingleton.class) {
               if(instance == null) {
                   instance = new LasySingleton();
                }
            }
	}
       return instance;
   }

   public void aService() {//do something}
} 

              该实现的初衷在于降低懒汉式单例方式同步化的开销,因为懒汉式初始化有效的仅为首次调用,但为了适应多线程环境,不得不同步化静态工厂方法,无疑加大了额外开销,若能缩小同步化范围(如上面的同步化代码块),可取得较佳的效果。上面的方式的确仍然可以保证只有一个单例类实例存在,但美好的初衷,依旧无法在java中得到满足。接下来我们再看两个问题:

              问题一:画蛇添足。 上面的这个技巧,第一次或第二次检查是否可以省略掉?答案是“否”:按照多线程原理和双重检查成例预想方案,它们是不可以省略掉的。

              问题二:“双重检查成例”懒汉单例为何在java语言中不成立(而对C语言成立)?答案是:双重检查成例对java语言编译器不成立。在java编译器中,LasySingleton类的初始化与instance变量赋值顺序是不可预料的。若一个线程在没有同步化的条件下读取instance引用,并调用这个对象的话,可能会发现对象的初始化尚未完成,从而造成崩溃。(比较怪异,个人认为,但不得不遵循最佳实践

              #实现方式之最佳实践:到目前为止,人们得出的结论是:一般而言,双重检查成例无法在现有的java语言编译器里工作(Joshua Bloch. Effectiv Java-Programming Language Guide. published by Addison-Wesley,2001)。一般情况下,单例模式可采用饿汉式单例模式或对整个静态工厂方法同步化的懒汉式单例模式。

             #单例模式在下列环境的局限性

                #局限性1:多个JVM系统的分布式系统:EJB容器有能力将一EJB的实例跨过多个JVM调用。因Singleton不是EJB型,故Singleton只局限于某一JVM中。也就是说,若EJB在跨过多个JVM后仍然需要引用同一个Singleton类的话,这个Singleton就会在数个JVM中被实例化,造成多个Singleton对象的实例出现。一个J2EE系统可能分布在数个JVM中,这时不一定需要EJB,就能造成多个Singleton的实例出现在不同的JVM中。若Singleton是无状态的,便没问题。应当注意,在任何使用了EJB、RMI和JNI技术的分布式系统中,应当避免使用有状态的Singleton模式。

                 #局限性2:多个类加载器:同一个JVM中会有多个类加载器,当两个类加载器同时加载一个类时,会出现两个实例。很多J2EE服务器允许同一个服务器内有几个servlet引擎时,每个引擎都有独立的类加载器,经由不同的类加载器加载的对象之间是绝缘的。比如,一个J2EE系统所在的J2EE服务器中有两个Servlet引擎:一个作为内网给公司的网站管理人员使用,另一个给公司的外部客户使用。两者共享同一个数据库,两个系统都需要调用同一个单例类。若该单例类是有状态的话,那么,内网和外网用户看到的单例类对象的状态就会不同。除非系统有协调机制,不然在这种情况下应当尽量避免使用有状态的单例类。

 

...待续...