转载 单例(Singleton)模式)的误区

      在创建型模式中,单例(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中“双重检查成例”单例的实现:

Java代码  
  1. package pattern.creational.singleton;   
  2.   
  3. public class LasySingleton {   
  4.    private static LasySingleton instance = null;   
  5.   
  6.    private LasySingleton() {}   
  7.   
  8.    public static LasySingleton getInstance() {   
  9.        if(instance == null) {   
  10.            synchronized(LasySingleton.class) {   
  11.                if(instance == null) {   
  12.                    instance = new LasySingleton();   
  13.                 }   
  14.             }   
  15.     }   
  16.        return instance;   
  17.    }   
  18.   
  19.    public void aService() {//do something}   
  20. }   

        该实现的初衷在于降低懒汉式单例方式同步化的开销,因为懒汉式初始化有效的仅为首次调用,但为了适应多线程环境,不得不同步化静态工厂方法,无疑加大了额外开销,若能缩小同步化范围(如上面的同步化代码块),可取得较佳的效果。上面的方式的确仍然可以保证只有一个单例类实例存在,但美好的初衷,依旧无法在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引擎:一个作为内网给公司的网站管理人员使用,另一个给公司的外部客户使用。两者共享同一个数据库,两个系统都需要调用同一个单例类。若该单例类是有状态的话,那么,内网和外网用户看到的单例类对象的状态就会不同。除非系统有协调机制,不然在这种情况下应当尽量避免使用有状态的单例类。

      在上面我们已经简单的阐述了大部分有关单例(Singleton)模式的知识点,主要包括何谓单例、单例使用场景、单例几种实现方式及其特点以及单例的局限性。接下来我们将着重阐述有关原型(Prototype)的一些常识,主要涉及原型的用意、实现形式,以及与之相关的clone条件与equals方法和深浅拷贝等问题。

      所谓原型(Prototype)模式,是指通过给出一个原型对象来指明所要创建的对象类型,然后用复制这个原型对象的办法创建出更多同类型的对象。下图左侧是原型模式的一般性结构示意图,右侧是简单的实现样例。

      原型(Prototype)模式主要采用的是java的clone克隆,克隆有克隆的前提(即满足的条件),也有深、浅之别。

      首先,我们来看看克隆满足的条件:1、对任何的对象x,都有 x.clone() != x。即克隆对象与原对象不是同一个对象。2、对任何的对象x,都有 x.clone().getClass() == x.getClass(),即克隆对象与原对象的类型是一样。3、如果对象x的equals()方法定义恰当的话,那么 x.clone().equals(x)应当是成立的。 在java语言的API中,凡是提供了clone()方法的类,都满足上面的这些条件。(一般来说,clone前两个条件必须,后一个可选)。

      其次,我们来看看深浅克隆(即拷贝)的问题。浅克隆是指仅仅克隆所考虑的对象,而不克隆它所引用的对象,而深克隆是指把要克隆的对象及其所引用的对象都克隆一遍,这种对引用到的对象的克隆称之为间接克隆。(注:深克隆要深入到多少层是一个不易确定的问题)。

      最后,我们再共同关注下克隆满足条件之3equals()方法。关于此条件的满足,很多人误认为可以通过继承得到java.lang.Object对象的equals()方法就足够了,这其实是一个误区点。在java的源码中java.lang.Object的equals()方法是这样的:

Java代码  
  1. public boolean equals(Object obj) {   
  2.     return (this == obj);   
  3. }  

     也就是说,这仅仅是地址空间而非对象内容的比较,当两个变量同时指向一个对象时,equals()方法才会返回true。很显然,这并不是适合所有需要被克隆的对象的。

     一般而言,为了做到深克隆,所有对象都需要实现java.Serializable这个串行化接口。在这里,我们简单地提一下利用序列化的方法做深克隆。

    记得在《java与模式》一书中讲到:把对象写到流里的过程是串行化过程(java程序师圈里戏称冷冻或腌咸菜),把对象从流中读出来的并行化过程称解冻或回鲜过程。应当指出,写到流里的是对象的一个拷贝,而原对象仍然存在于JVM里面,因此,“腌成咸菜”的只是对象的一个拷贝,java咸菜还可以回鲜。实现过程的核心代码大致为:

Java代码  
  1. //deeply clone   
  2. public Object clone(){   
  3.       ByteArrayOutputStream bo = new ByteArrayOutputStream();   
  4.       ObjectOutputStream oo = new ObjectOutputStream(bo);   
  5.       oo.writeObject(this);   
  6.       ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());   
  7.      ObjectInputStream oi = new ObjectInputStream(bi);   
  8.      return oi.readObject();   
  9. }  

posted on 2017-12-13 11:01  wanglgkaka  阅读(197)  评论(0编辑  收藏  举报

导航