剑指OFFER(java)-设计一个只能建立一个惟一实例的类——单例模式

       对于一个软件系统的某些类而言,咱们无须建立多个实例。举个你们都熟知的例子——Windows任务管理器, 一般状况下,不管咱们启动任务管理多少次,Windows系统始终只能弹出一个任务管理器窗口,也就是说在一个Windows系统中,任务管理器存在惟一性。 还有好比说窗口上的工具箱,若是每次点击工具箱按钮都会建立一个工具箱实例,那么窗口中会出现不少工具箱,可是咱们想要的是点击工具箱若是没有就出现,有了就再也不出现了,这就须要用到单例模式。html

    

 下面是饿汉式单例类:     android

package cglib;编程

public class SingletonClass {安全

    private static final SingletonClass instance=new SingletonClass();
    //私有构造函数
    private SingletonClass(){}多线程

//禁止类的外部直接使用new来建立对象,所以须要将SingletonClass的构造函数的可见性改成//private
    public static SingletonClass getInstance(){
    return instance;
    }
    
    public static void main(String[] args){
        SingletonClass s1=new SingletonClass();
        SingletonClass s2=new SingletonClass();
        SingletonClass s3=SingletonClass.getInstance();
        SingletonClass s4=SingletonClass.getInstance();
        System.out.println("s1:"+s1);
        System.out.println("s2:"+s2);
        System.out.println("s3:"+s3);
        System.out.println("s4:"+s4);
        System.out.println("instance:"+instance);
        if(s3==s4){
            System.out.println("s3,s4两个实例相同");
            System.out.println("s3:"+s3);
            System.out.println("s4:"+s4);
        }
    }
    
}并发

输出:编程语言

s1:cglib.SingletonClass@139a55
s2:cglib.SingletonClass@1db9742
s3:cglib.SingletonClass@106d69c
s4:cglib.SingletonClass@106d69c
instance:cglib.SingletonClass@106d69c
s3,s4两个实例相同
s3:cglib.SingletonClass@106d69c
s4:cglib.SingletonClass@106d69c函数

 

须要注意的是getInstance()方法的修饰符,首先它应该是一个public方法,以便供外界其余对象使用,其次它使用了static关键字,即它是一个静态方法,在类外能够直接经过类名来访问,而无须建立 SingletonClass 对象,事实上在类外也没法建立 SingletonClass 对象,由于构造函数是私有的。高并发

在类外咱们没法直接建立新的 SingletonClass 对象,但能够经过代码 SingletonClass .getInstance()来访问实例对象,第一次调用getInstance()方法时将建立惟一实例,再次调用时将返回第一次建立的实例,从而确保实例对象的惟一性。工具

因此:单例模式定义以下: 

单例模式(Singleton Pattern):确保某一个类只有一个实例,并且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象建立型模式。

      单例模式有三个要点:一是某个类只能有一个实例;二是它必须自行建立这个实例;三是它必须自行向整个系统提供这个实例。

Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户能够访问它的惟一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,做为外部共享的惟一实例。

       饿汉式单例因为在定义静态变量的时候实例化单例类,所以在类加载的时候就已经建立了单例对象,当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的惟一实例将被建立,可确保单例对象的惟一性。

下面是懒汉式单例模式:

package cglib;

public class SingletonClass {

    private static SingletonClass instance=null;
    //私有构造函数
    private SingletonClass(){}
    public synchronized static SingletonClass getInstance(){
    if(instance==null){
    System.out.println("第一次调用为空instance:"+instance);
    instance=new SingletonClass();
    System.out.println("建立完instance:"+instance);
    }
    
    return instance;
    }
    
    public static void main(String[] args){
        SingletonClass s1=new SingletonClass();
        SingletonClass s2=new SingletonClass();
        SingletonClass s3=SingletonClass.getInstance();
        SingletonClass s4=SingletonClass.getInstance();
        System.out.println("s1:"+s1);
        System.out.println("s2:"+s2);
        System.out.println("s3:"+s3);
        System.out.println("s4:"+s4);
        System.out.println("instance:"+instance);
        if(s3==s4){
            System.out.println("s3,s4两个实例相同");
            System.out.println("s3:"+s3);
            System.out.println("s4:"+s4);
        }
    }
    
}


输出:

第一次调用为空instance:null
建立完instance:cglib.SingletonClass@139a55
s1:cglib.SingletonClass@1db9742
s2:cglib.SingletonClass@106d69c
s3:cglib.SingletonClass@139a55
s4:cglib.SingletonClass@139a55
instance:cglib.SingletonClass@139a55
s3,s4两个实例相同
s3:cglib.SingletonClass@139a55
s4:cglib.SingletonClass@139a55

懒汉式单例在第一次调用getInstance()方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载(Lazy Load)技术,即须要的时候再加载实例,为了不多个线程同时调用getInstance()方法,咱们可使用关键字synchronized

该懒汉式单例类在getInstance()方法前面增长了关键字synchronized进行线程锁,以处理多个线程同时访问的问题。可是,上述代码虽然解决了线程安全问题,可是每次调用getInstance()时都须要进行线程锁定判断,在多线程高并发访问环境中,将会致使系统性能大大下降。如何既解决线程安全问题又不影响系统性能呢?咱们继续对懒汉式单例进行改进。事实上,咱们无须对整个getInstance()方法进行锁定,只需对其中的代码“instance = new SingletonClass ();”进行锁定便可。所以getInstance()方法能够进行以下改进:

  1. public static  SingletonClass getInstance() {   
  2.     if (instance == null) {  
  3.         synchronized ( SingletonClass .class) {  
  4.             instance = new  SingletonClass ();   
  5.         }  
  6.     }  
  7.     return instance;   

问题貌似得以解决,事实并不是如此。若是使用以上代码来实现单例,仍是会存在单例对象不惟一。缘由以下:

      假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能经过instance == null的判断。因为实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例建立代码,线程B处于排队等待状态,必须等待线程A执行完毕后才能够进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经建立,将继续建立新的实例,致使产生多个单例对象,违背单例模式的设计思想,所以须要进行进一步改进,在synchronized中再进行一次(instance == null)判断,这种方式称为双重检查锁定(Double-Check Locking)。使用双重检查锁定实现的懒汉式单例类完整代码以下所示:

  1. public  class  SingletonClass {   
  2.     private volatile static  SingletonClass instance = null;   
  3.   
  4.     private  SingletonClass () { }   
  5.   
  6.     public static  SingletonClass getInstance() {   
  7.         //第一重判断  
  8.         if (instance == null) {  
  9.             //锁定代码块  
  10.             synchronized ( SingletonClass .class) {  
  11.                 //第二重判断  
  12.                 if (instance == null) {  
  13.                     instance = new  SingletonClass (); //建立单例实例  
  14.                 }  
  15.             }  
  16.         }  
  17.         return instance;   
  18.     }  

须要注意的是,若是使用双重检查锁定来实现懒汉式单例类,须要在静态成员变量instance以前增长修饰符volatile,被volatile修饰的成员变量能够确保多个线程都可以正确处理,且该代码只能在JDK 1.5及以上版本中才能正确执行。因为volatile关键字会屏蔽Java虚拟机所作的一些代码优化,可能会致使系统运行效率下降,所以即便使用双重检查锁定来实现单例模式也不是一种完美的实现方式。

 

饿汉式单例类与懒汉式单例类比较

      饿汉式单例类在类被加载时就将本身实例化,它的优势在 于无须考虑多线程访问问题,能够确保实例的惟一性;从调用速度和反应时间角度来说,因为单例对象一开始就得以建立,所以要优于懒汉式单例。可是不管系统在 运行时是否须要使用该单例对象,因为在类加载时该对象就须要建立,所以从资源利用效率角度来说,饿汉式单例不及懒汉式单例,并且在系统加载时因为须要建立 饿汉式单例对象,加载时间可能会比较长。

      懒汉式单例类在 第一次使用时建立,无须一直占用系统资源,实现了延迟加载,可是必须处理好多个线程同时访问的问题,特别是当单例类做为资源控制器,在实例化时必然涉及资 源初始化,而资源初始化颇有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,须要经过双重检查锁定等机制进行控制,这将致使系统性 能受到必定影响。

一种更好的单例实现方法

饿汉式单例类不能实现延迟加载,无论未来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,并且性能受影响。下面咱们来学习更好的被称之为Initializationon Demand Holder (IoDH)的技术。

      在IoDH中,咱们在单例类中增长一个静态(static)内部类,在该内部类中建立单例对象,再将该单例对象经过getInstance()方法返回给外部使用,实现代码以下所示:

package cglib;

public class SingletonClass {

     private  SingletonClass () {  
        }  
          
        private static class  SingletonClassHolder {  
                private final static  SingletonClass instance = new  SingletonClass ();  
        }  
          
        public static  SingletonClass getInstance() {  
            return  SingletonClassHolder.instance;  
        }  
    
    
    
    public static void main(String[] args){
        SingletonClass s1=new SingletonClass();
        SingletonClass s2=new SingletonClass();
        SingletonClass s3=SingletonClass.getInstance();
        SingletonClass s4=SingletonClass.getInstance();
        System.out.println("s1:"+s1);
        System.out.println("s2:"+s2);
        System.out.println("s3:"+s3);
        System.out.println("s4:"+s4);
        System.out.println("SingletonClassHolder.instance:"+SingletonClassHolder.instance);
        if(s3==s4){
            System.out.println("s3,s4两个实例相同");
            System.out.println("s3:"+s3);
            System.out.println("s4:"+s4);
        }

输出:

s1:cglib.SingletonClass@139a55
s2:cglib.SingletonClass@1db9742
s3:cglib.SingletonClass@106d69c
s4:cglib.SingletonClass@106d69c
SingletonClassHolder.instance:cglib.SingletonClass@106d69c
s3,s4两个实例相同
s3:cglib.SingletonClass@106d69c
s4:cglib.SingletonClass@106d69c

 

 

 

       编译并运行上述代码,运行结果为:true,即建立的单例对象s1和s2为同一对象。因为静态单例对象没有做为 SingletonClass 的成员变量直接实例化,所以类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类 SingletonClassHolder ,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。因为getInstance()方法没有任何线程锁定,所以其性能不会形成任何影响。

 

 

      经过使用IoDH,咱们既能够实现延迟加载,又能够保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言自己的特性相关,不少面向对象语言不支持IoDH)。

 

     

可是,上面提到的全部实现方式都有两个共同的缺点:

  • 都须要额外的工做(Serializable、transient、readResolve())来实现序列化,不然每次反序列化一个序列化的对象实例时都会建立一个新的实例。
  • 可能会有人使用反射强行调用咱们的私有构造器(若是要避免这种状况,能够修改构造器,让它在建立第二个实例的时候抛异常)。

枚举写法

固然,还有一种更加优雅的方法来实现单例模式,那就是枚举写法:

public enum Singleton {
        INSTANCE;
        private String name;
        public String getName(){
            return name;
        }
        public void setName(String name){
            this.name = name;
        }
    }

使用枚举除了线程安全和防止反射强行调用构造器以外,还提供了自动序列化机制,防止反序列化的时候建立新的对象。所以,Effective Java推荐尽量地使用枚举来实现单例。

可是在Android平台上倒是不被推荐的。在这篇Android Training中明确指出:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

再好比双重检查锁法,不能在jdk1.5以前使用,而在Android平台上使用就比较放心了(通常Android都是jdk1.6以上了,不只修正了volatile的语义问题,还加入了很多锁优化,使得多线程同步的开销下降很多)。

因此,无论采起何种方案,请时刻牢记单例的三大要点:

  • 线程安全
  • 延迟加载
  • 序列化与反序列化安全

 

单例模式总结

      

1.主要优势

       单例模式的主要优势以下:

       (1) 单例模式提供了对惟一实例的受控访问。由于单例类封装了它的惟一实例,因此它能够严格控制客户怎样以及什么时候访问它。

       (2) 因为在系统内存中只存在一个对象,所以能够节约系统资源,对于一些须要频繁建立和销毁的对象单例模式无疑能够提升系统的性能。

       (3) 容许可变数目的实例。基于单例模式咱们能够进行扩展,使用与单例控制类似的方法来得到指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。

 

2.主要缺点

       单例模式的主要缺点以下:

       (1) 因为单例模式中没有抽象层,所以单例类的扩展有很大的困难。

       (2) 单例类的职责太重,在必定程度上违背了“单一职责原则”。由于单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的建立和产品的自己的功能融合到一块儿。

       (3) 如今不少面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,所以,若是实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将从新实例化,这将致使共享的单例对象状态的丢失。

 

3.适用场景

       在如下状况下能够考虑使用单例模式:

       (1) 系统只须要一个实例对象,如系统要求提供一个惟一的序列号生成器或资源管理器,或者须要考虑资源消耗太大而只容许建立一个对象。

       (2) 客户调用类的单个实例只容许使用一个公共访问点,除了该公共访问点,不能经过其余途径访问该实例。

相关文章
相关标签/搜索