[Java] [设计模式] - 单列模式几种实现的区分

单例模式概念

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

单例模式的几种实现方式

单例模式的实现有多种方式,以下所示:编程

一、懒汉式,线程不安全

是否 Lazy 初始化:是
是否多线程安全:否
实现难度:易
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。由于没有加锁 synchronized,因此严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工做。设计模式

代码实例:缓存

public class LazySingleton {  
    private static LazySingleton instance = null;  
    /** 私有默认构造子 */  
    private LazySingleton(){}  
    /** 静态工厂方法 */  
    public static LazySingleton getInstance(){  
        if(instance == null){  
            instance = new LazySingleton();  
        }  
        return instance;  
    }  
}

  懒汉式实际上是一种比较形象的称谓。既然懒,那么在建立对象实例的时候就不着急。会一直等到立刻要使用对象实例的时候才会建立,懒人嘛,老是推脱不开的时候才会真正去执行工做,所以在装载对象的时候不建立对象实例。安全

private static LazySingleton instance = null;

  懒汉式是典型的时间换空间,就是每次获取实例都会进行判断,看是否须要建立实例,浪费判断的时间。固然,若是一直没有人使用的话,那就不会建立实例,则节约内存空间。多线程

接下来介绍的几种实现方式都支持多线程,可是在性能上有所差别。并发

二、懒汉式,线程安全

是否 Lazy 初始化:是
是否多线程安全:是
实现难度:易
描述:这种方式具有很好的 lazy loading,可以在多线程中很好的工做,可是,效率很低,99% 状况下不须要同步。
优势:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。框架

代码实例:编程语言

public class LazySingleton {  
    private static LazySingleton instance = null;  
    /** 私有默认构造子 */  
    private LazySingleton(){}  
    /** 静态工厂方法 */  
    public static synchronized LazySingleton getInstance(){  
        if(instance == null){  
            instance = new LazySingleton();  
        }  
        return instance;  
    }  
}

  上面的懒汉式单例类实现里对静态工厂方法使用了同步化,以处理多线程环境。性能

  因为懒汉式的实现是线程安全的,这样会下降整个访问的速度,并且每次都要判断。那么有没有更好的方式实现呢?

三、饿汉式

是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种方式比较经常使用,但容易产生垃圾对象。
优势:没有加锁,执行效率会提升。
缺点:类加载时就初始化,浪费内存。
它基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然致使类装载的缘由有不少种,在单例模式中大多数都是调用getInstance方法, 可是也不能肯定有其余的方式(或者其余的静态方法)致使类装载,这时候初始化instance显然没有达到lazy loading的效果。

代码实例:

public class EagerSingleton {  
    private static EagerSingleton instance = new EagerSingleton();  
    /** 私有默认构造子*/  
    private EagerSingleton(){}  
    /** 静态工厂方法*/  
    public static EagerSingleton getInstance(){  
        return instance;  
    }  
}

 上面的例子中,在这个类被加载时,静态变量instance会被初始化,此时类的私有构造子会被调用。这时候,单例类的惟一实例就被建立出来了。

  饿汉式实际上是一种比较形象的称谓。既然饿,那么在建立对象实例的时候就比较着急,饿了嘛,因而在装载类的时候就建立对象实例。

private static EagerSingleton instance = new EagerSingleton();

  饿汉式是典型的空间换时间,当类装载的时候就会建立类的实例,无论你用不用,先建立出来,而后每次调用的时候,就不须要再判断,节省了运行时间。

四、双检锁/双检查锁(DCL,即 double-checked locking)

JDK 版本:JDK1.5 起
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂
描述:这种方式称为双重检查锁(Double-Check Locking),须要注意的是,若是使用双重检查锁定来实现懒汉式单例类,须要在静态成员变量instance以前增长修饰符volatile它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,全部对该变量的读写都是直接操做共享内存,从而确保多个线程能正确的处理该变量,且该代码只能在JDK 1.5及以上版本中才能正确执行。因为volatile关键字会屏蔽Java虚拟机所作的一些代码优化,可能会致使系统运行效率下降,所以即便使用双重检查锁定来实现单例模式也不是一种完美的实现方式。

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

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

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

代码实例:

public class Singleton {  
    private volatile static Singleton instance = null;  
    private Singleton(){}  
    public static Singleton getInstance(){  
    //先检查实例是否存在,若是不存在才进入下面的同步块  
    if(instance == null){  // @1
        //同步块,线程安全的建立实例,用的是类同步锁(全局锁)  
        synchronized (Singleton.class) {  
            //再次检查实例是否存在,若是不存在才真正的建立实例,防止已经进入@1的阻塞线程不知道instance状态改变了  
            if(instance == null){  
               instance = new Singleton();  
            }  
        }  
    }  
    return instance;  
    }  
}

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

  提示因为volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,因此运行效率并非很高。所以通常建议,没有特别的须要,不要使用。也就是说,虽然可使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,能够根据状况来选用。

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

五、静态内部类(Lazy initialization holder class模式)

是否 Lazy 初始化:是
是否多线程安全:是
实现难度:通常
描述:饿汉式单例类不能实现延迟加载,无论未来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,并且性能受影响。可见,不管是饿汉式单例仍是懒汉式单例都存在这样那样的问题,有没有一种方法,可以将两种单例的缺点都克服,而将二者的优势合二为一呢?答案是:Yes!下面咱们来学习这种更好的被称之为Initialization Demand Holder (IoDH)的技术。在IoDH中,咱们在单例类中增长一个静态(static)内部类,在该内部类中建立单例对象,再将该单例对象经过getInstance()方法返回给外部使用。因为静态单例对象没有做为Singleton的成员变量直接实例化,所以类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类SingletonHolder,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。因为getInstance()方法没有任何线程锁定,所以其性能不会形成任何影响。经过使用IoDH,咱们既能够实现延迟加载,又能够保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式**(其缺点是与编程语言自己的特性相关,不少面向对象语言不支持IoDH)。

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

1.相应的基础知识

  •  什么是类级内部类?

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

  类级内部类至关于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,所以可直接建立。而对象级内部类的实例,是绑定在外部对象实例中的。

  类级内部类中,能够定义静态的方法。在静态方法中只可以引用外部类中的静态成员方法或者静态成员变量。

  类级内部类至关于其外部类的成员,只有在第一次被使用的时候才被会装载。

  •  多线程缺省同步锁的知识

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

  1.由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时

  2.访问final字段时

  3.在建立线程以前建立对象时

  4.线程能够看见它将要处理的对象时

2.解决方案的思路

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

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

示例代码以下:

public class Singleton {  
    private Singleton(){}  
    /** 
     *  类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例 
     *  没有绑定关系,并且只有被调用到时才会装载,从而实现了延迟加载。 
     */  
    private static class SingletonHolder{  
        /** 静态初始化器,由JVM来保证线程安全*/  
        private static final Singleton INSTANCE = new Singleton();  
    }  
      
    public static Singleton getInstance(){  
        return SingletonHolder.INSTANCE;  
    }  
}

  因为 SingletonHolder 是私有的,除了 getInstance() 以外没有办法访问它,所以它是懒汉式的;当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.INSTANCE从而使SingletonHolder静态内部类获得初始化;而这个内部类在装载并被初始化的时候,会初始化它的静态域,从而建立Singleton的实例,因为是静态的域,所以只会在虚拟机装载类的时候初始化一次,并由JVM自己机制保证了线程安全问题

  这个模式的优点在于,getInstance方法并无被同步,而且只是执行一个域的访问,所以延迟初始化并无增长任何访问成本。同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

六、枚举

JDK 版本:JDK1.5 起

是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种实现方式尚未被普遍采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止屡次实例化。
这种方式是Effective Java做者Josh Bloch提倡的方式,它不只能避免多线程同步问题,并且还自动支持序列化机制,防止反序列化从新建立新的对象,绝对防止屡次实例化。不过,因为 JDK1.5 以后才加入 enum 特性,用这种方式写难免让人感受生疏,在实际工做中,也不多用。
不能经过reflection attack来调用私有构造方法。

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

代码实例:

public enum Singleton {  
    /** 定义一个枚举的元素,它就表明了Singleton的一个实例。*/  
    uniqueInstanceA;  //实例      
    uniqueInstanceA(2,"singleton2");  //实例    
    int id;//私有变量
    String name;
    
    Singleton(int id, String name) {//构造方法
        this.id= id;        
        this.name = name;
    }
    /** 单例能够有本身的操做 */  
    public void singletonOperation(){  
        //功能处理  
    }  
}  
//调用
public static void main(String[] args) {
    Singleton singleton1 = Singleton.uniqueInstanceA;
    Singleton singleton2 = Singleton.uniqueInstanceA;
    System.out.println(singleton1 == singleton2); // true
    singleton1.singletonOperation(); // 单例能够有本身的操做 
}

  使用枚举来实现单实例控制会更加简洁,并且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止屡次实例化,是更简洁、高效、安全的实现单例的方式。

经验之谈

通常状况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现lazy loading效果时,才会使用第 5 种登记方式。若是涉及到反序列化建立对象时,能够尝试使用第 6 种枚举方式。若是有其余特殊的需求,能够考虑使用第 4 种双检锁方式。

总结

单例模式做为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率至关高,在不少应用软件和框架中都得以普遍应用。

1.主要优势

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

  • 单例模式提供了对惟一实例的受控访问。由于单例类封装了它的惟一实例,因此它能够严格控制客户怎样以及什么时候访问它。
  • 因为在系统内存中只存在一个对象,所以能够节约系统资源,对于一些须要频繁建立和销毁的对象单例模式无疑能够提升系统的性能。
  • 容许可变数目的实例。基于单例模式咱们能够进行扩展,使用与单例控制类似的方法来得到指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。

2.主要缺点

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

  • 因为单例模式中没有抽象层,所以单例类的扩展有很大的困难。
  • 单例类的职责太重,在必定程度上违背了“单一职责原则”。由于单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的建立和产品的自己的功能融合到一块儿。
  • 如今不少面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,所以,若是实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将从新实例化,这将致使共享的单例对象状态的丢失。

3.适用场景

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

  • 系统只须要一个实例对象,如系统要求提供一个惟一的序列号生成器或资源管理器,或者须要考虑资源消耗太大而只容许建立一个对象。
  • 客户调用类的单个实例只容许使用一个公共访问点,除了该公共访问点,不能经过其余途径访问该实例。


参考博客


嘟嘟MD:http://www.jianshu.com/p/d8bf5d08a147

相关文章
相关标签/搜索