单例模式解析(二)

来源:互联网 发布:淘宝发布宝贝图片大小 编辑:程序博客网 时间:2024/06/16 12:41

单例模式是最常见的一个模式,在Java中单例模式被大量的使用。这同样也是我在面试时最喜欢提到的一个面试问题,然后在面试者回答后可以进一步挖掘其细节,这不仅检查了关于单例模式的相关知识,同时也检查了面试者的编码水平、多线程方面的知识,这些在实际的工作中非常重要。

在这个简单的Java面试教程中,我列举了一些Java面试过程中关于单例模式的常会被提到的问题。关于这些面试问题,我没有提供答案,因为你通过百度搜索很容易找到这些答案。

那么问题就从什么是单例模式?你之前用过单例模式吗?
开始

  定义:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

   类型:创建类模式


类图知识点:

1.类图分为三部分,依次是类名、属性、方法

2.以<<开头和以>>结尾的为Stereotype

3.修饰符+代表public,-代表private,#代表protected,什么都没有代表包可见。

4.带下划线的属性或方法代表是静态的。

5.对类图中对象的关系不熟悉的朋友可以参考文章:设计模式中类的关系

单例模式应该是23种设计模式中最简单的一种模式了。它有以下几个要素:

  • 私有的构造方法
  • 指向自己实例的私有静态引用
  • 以自己实例为返回值的静态的公有的方法

  单例模式根据实例化对象时机的不同分为两种:一种是饿汉式单例,一种是懒汉式单例。饿汉式单例在单例类被加载时候,就实例化一个对象交给自己的引用;而懒汉式在调用取得实例方法的时候才会实例化对象。代码如下:


   Eager mode:

[java] view plain copy
  1. class Singleton{    
  2.     private Singleton(){}    
  3.     private static final Singleton singleton = new Singleton();    
  4.     public static Singleton getInstance(){return singleton;}    
  5. }    
  Lazy mode:
[java] view plain copy
  1. class Singleton{    
  2.     private Singleton(){}    
  3.     private static Singleton singleton ;    
  4.     public static synchronized Singleton getInstance(){    
  5.         if(singleton==null)    
  6.             singleton = new Singleton();    
  7.         return singleton;           
  8.     }       
  9. }    

1) 哪些类是单例模式的候选类?在Java中哪些类会成为单例?

  (1) 系统资源,如文件路径,数据库链接,系统常量等

  (2)全局状态化类,类似AutomicInteger的使用

 

单例模式的优点:

  • 在内存中只有一个对象,节省内存空间。
  • 避免频繁的创建销毁对象,可以提高性能。
  • 避免对共享资源的多重占用。
  • 可以全局访问。

适用场景:由于单例模式的以上优点,所以是编程中用的比较多的一种设计模式。我总结了一下我所知道的适合使用单例模式的场景:

  • 需要频繁实例化然后销毁的对象。
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 有状态的工具类对象。
  • 频繁访问数据库或文件的对象。

  这里将检查面试者是否有对使用单例模式有足够的使用经验。他是否熟悉单例模式的优点和缺点。

面试归纳:servlet就是单例多线程,启动容器时(默认情况下)会调用init()方法初始化servlet,且该方法只调用一次,处理请求时调用service()方法,当处理多个不同的请求时,会将请求进行分发,创建不同的实例进行处理,所以为单例多线程,而连接池也是单例的,创建一些连接放入连接池,使用时取,用完后不用关闭,返回给连接池,这样就不用频繁的创建和关闭连接(连接池作用:连接的重用和管理连接)。


2)你能在Java中编写单例里的getInstance()的代码?

很多面试者都在这里失败。然而如果不能编写出这个代码,那么后续的很多问题都不能被提及。

   

  (1)静态成员直接初始化,或者在静态代码块初始化都可以

[java] view plain copy
  1. class Singleton{    
  2.     private Singleton(){}    
  3.     private static Singleton singleton ;    
  4.     public static synchronized Singleton getInstance(){    
  5.         if(singleton==null)    
  6.             singleton = new Singleton();    
  7.         return singleton;           
  8.     }       
  9. }    
  该实现只要在一个ClassLoad下就会提供一个对象的单例。但是美中不足的是,不管该资源是否被请求,它都会创建一个对象,占用jvm内存。饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间。

从lazy initialization思想出发,出现了下2的写法

  (2) 根据lazy initialization思想,使用到时才初始化。

[java] view plain copy
  1. class Singleton{    
  2.     private Singleton(){}    
  3.     private static Singleton singleton ;    
  4.     public static synchronized Singleton getInstance(){    
  5.         if(singleton==null)    
  6.             singleton = new Singleton();    
  7.         return singleton;           
  8.     }       
  9. }    
  该实现方法加了同步锁,可以有效防止多线程在执行getInstance方法得到2个对象。

缺点:只有在第一次调用的时候,才会出现生成2个对象,才必须要求同步。而一旦singleton 不为null,系统依旧花费同步锁开销,有点得不偿失。

因此再改进出现写法3

[java] view plain copy
  1. class Singleton{    
  2.     private Singleton(){}    
  3.     private static Singleton singleton ;    
  4.     public static Singleton getInstance(){    
  5.         if(singleton==null)//1    
  6.             synchronized(Singleton.class){//2    
  7.                 singleton = new Singleton();//3    
  8.             }    
  9.         return singleton;           
  10.     }       
  11. }    

这种写法减少了锁开销,但是在如下情况,却创建了2个对象:

a:线程1执行到1挂起,线程1认为singleton为null

b:线程2执行到1挂起,线程2认为singleton为null

c:线程1被唤醒执行synchronized块代码,走完创建了一个对象

d:线程2被唤醒执行synchronized块代码,走完创建了另一个对象

所以看出这种写法,并不完美。  

(4)为了解决3存在的问题,引入双重检查锁定

 
[java] view plain copy
  1. public static Singleton getInstance(){    
  2.         if(singleton==null)//1    
  3.             synchronized(Singleton.class){//2    
  4.                 if(singleton==null)//3    
  5.                     singleton = new Singleton();//4    
  6.             }    
  7.         return singleton;           
  8.     }   

      在同步锁代码块内部,再判断一次对象是否为null,为null才创建对象。这种写法已经接近完美:

a:线程1执行到1,已经进入synchronized的时候,线程挂起,线程1占有Singleton.class资源锁;

b:线程2执行到1,当它准备synchronized块时,因为Singleton.class被占用,线程2阻塞;

c:线程1被唤醒,判断出对象为null,执行完创建一个对象

d:线程2被唤醒,判断出对象不为null,不执行创建语句

      如此分析,发现似乎没问题。

      但是实际上并不能保证它在单处理器或多处理器上正确运行;

      问题就出现在singleton = new Singleton()这一行代码。它可以简单的分成如下三个步骤: 

[java] view plain copy
  1. mem= singleton();//1  
  2. instance = mem;//2  
  3. ctorSingleton(instance);//3  

  这行代码先在内存开辟空间,赋给singleton的引用,然后执行new 初始化数据,但是注意初始化是要消耗时间。如果此时线程3在执行步骤1的时候,发现singleton 为非null,就直接返回,那么线程3返回的其实是一个没构造完成的对象。

      我们期望1,2,3 按照反序执行,但是实际jvm内存模型,并没有明确的有序指定。

      这归咎于java的平台的内存模型允许“无序写入”。

 (5) 在4的基础上引入volatile

代码如下:

[java] view plain copy
  1. class Singleton{    
  2.     private Singleton(){}    
  3.     private static volatile Singleton singleton ;    
  4.     public static Singleton getInstance(){    
  5.         if(singleton==null)//1    
  6.             synchronized(Singleton.class){//2    
  7.                 if(singleton==null)//3    
  8.                     singleton = new Singleton();    
  9.             }    
  10.         return singleton;           
  11.     }       
  12. }    

    Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。

   这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。

  根据上面的分析,常见的两种单例实现方式都存在小小的缺陷,那么有没有一种方案,既能实现延迟加载,又能实现线程安全呢?

  (6) Lazy initialization holder class模式

  这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。
  1.相应的基础知识
   什么是类级内部类?

  简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。

  •   类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。
  •   类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。
  •   类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。    

  多线程缺省同步锁的知识


  大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:

  1.由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
  2.访问final字段时
  3.在创建线程之前创建对象时
  4.线程可以看见它将要处理的对象时
  2.解决方案的思路

  要想很简单地实现线程安全,可以采用静态初始化器的方式,它可以由JVM来保证线程的安全性。比如前面的饿汉式实现方式。但是这样一来,不是会浪费一定的空间吗?因为这种实现方式,会在类装载的时候就初始化对象,不管你需不需要。

  如果现在有一种方法能够让类装载的时候不去初始化对象,那不就解决问题了?一种可行的方式就是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,那就不会创建对象实例,从而同时实现延迟加载和线程安全。
  示例代码如下:

[java] view plain copy
  1. public class Singleton {  
  2.       
  3.     private Singleton(){}  
  4.     /** 
  5.      *    类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例 
  6.      *    没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载。 
  7.      */  
  8.     private static class SingletonHolder{  
  9.         /** 
  10.          * 静态初始化器,由JVM来保证线程安全 
  11.          */  
  12.         private static Singleton instance = new Singleton();  
  13.     }  
  14.       
  15.     public static Singleton getInstance(){  
  16.         return SingletonHolder.instance;  
  17.     }  
  18. }  
  
   (6) 单例和枚举

   按照《高效Java 第二版》中的说法:单元素的枚举类型已经成为实现Singleton的最佳方法。用枚举来实现单例非常简单,只需要编写一个包含单个元素的枚举类型即可。
   

[java] view plain copy
  1. public enum Singleton {  
  2.     /** 
  3.      * 定义一个枚举的元素,它就代表了Singleton的一个实例。 
  4.      */  
  5.       
  6.     uniqueInstance;  
  7.       
  8.     /** 
  9.      * 单例可以有自己的操作 
  10.      */  
  11.     public void singletonOperation(){  
  12.         //功能处理  
  13.     }  
  14. }  
   使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

3)在getInstance()方法上同步有优势还是仅同步必要的块更优优势?你更喜欢哪个方式?

这确实是一个非常好的问题,我几乎每次都会提该问题,用于检查面试者是否会考虑由于锁定带来的性能开销。因为锁定仅仅在创建实例时才有意义,然后其他时候实例仅仅是只读访问的,因此只同步必要的块的性能更优,并且是更好的选择。

  缺点:只有在第一次调用的时候,才会出现生成2个对象,才必须要求同步。而一旦singleton 不为null,系统依旧花费同步锁开销,有点得不偿失。



4)什么是单例模式的延迟加载或早期加载?你如何实现它?

这是和Java中类加载的载入和性能开销的理解的又一个非常好的问题。我面试过的大部分面试者对此并不熟悉,但是最好理解这个概念。

5) Java平台中的单例模式的实例有哪些?

这是个完全开放的问题,如果你了解JDK中的单例类,请共享给我。

   java.lang.Runtime;

6) 单例模式的两次检查锁是什么?


   可以使用“双重检查加锁(double checked locking)”的方式来实现,就可以既实现线程安全,又能够使性能不受很大的影响。那么什么是“双重检查加锁”机制呢?

  所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

  “双重检查加锁”机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。Volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。它在某些情况下比synchronized的开销更小

  注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只只能用在java5及以上的版本。



7)你如何阻止使用clone()方法创建单例实例的另一个实例?

该类型问题有时候会通过如何破坏单例或什么时候Java中的单例模式不是单例来被问及。

在JAVA里要注意的是,所有的类都默认的继承自Object,所以都有一个clone方法。为保证只有一个实例,要把这个口堵上。有两个方面,一个是单例类一定要是final的,这样用户就不能继承它了。另外,如果单例类是继承于其它类的,还要override它的clone方法,让它抛出异常。


8)如果阻止通过使用反射来创建单例类的另一个实例?

开放的问题。在我的理解中,从构造方法中抛出异常可能是一个选项。

  通过反射创建单例类的另一个实例:

  如果借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器,反射攻击:   

[java] view plain copy
  1. public final class HelloWorld  
  2. {  
  3. private static HelloWorld instance = null;  
  4.    
  5. private HelloWorld()  
  6. {  
  7. }  
  8.    
  9. public static HelloWorld getInstance()  
  10. {  
  11. if (instance == null)  
  12. {  
  13. instance = new HelloWorld();  
  14. }  
  15. return instance;  
  16. }  
  17.    
  18. public void sayHello()  
  19. {  
  20. System.out.println("hello world!!");  
  21. }  
  22.    
  23. public static void sayHello2()  
  24. {  
  25. System.out.println("hello world 222 !!");  
  26. }  
  27.    
  28. static class Test  
  29. {  
  30. public static void main(String[] args) throws Exception  
  31. {  
  32. try  
  33. {  
  34. Class class1 = Class.forName("HelloWorld");  
  35. Constructor[] constructors = class1.getDeclaredConstructors();  
  36. AccessibleObject.setAccessible(constructors, true);  
  37. for (Constructor con : constructors)  
  38. {  
  39. if (con.isAccessible())  
  40. {  
  41. Object classObject = con.newInstance();  
  42. Method method = class1.getMethod("sayHello");  
  43. method.invoke(classObject);  
  44. }  
  45. }  
  46.    
  47. }  
  48. catch (Exception e)  
  49. {  
  50. e.printStackTrace();  
  51. }  
  52. }  
  53. }  
  54. }