在咱们平常的工做中常常须要在应用程序中保持一个惟一的实例,如:IO处理,数据库操做等,因为这些对象都要占用重要的系统资源,因此咱们必须限制这些实例的建立或始终使用一个公用的实例,这就是咱们今天要介绍的——单例模式(Singleton)。html
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于建立型模式,它提供了一种建立对象的最佳方式。java
这种模式涉及到一个单一的类,该类负责建立本身的对象,同时确保只有单个对象被建立。这个类提供了一种访问其惟一的对象的方式,能够直接访问,不须要实例化该类的对象。程序员
注意:数据库
咱们知道,一个类的对象的产生是由类构造函数来完成的。若是一个类对外提供了public
的构造方法,那么外界就能够任意建立该类的对象。因此,若是想限制对象的产生,一个办法就是将构造函数变为私有的(至少是受保护的),使外面的类不能经过引用来产生对象。同时为了保证类的可用性,就必须提供一个本身的对象以及访问这个对象的静态方法。设计模式
实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个得到该实例的方法(必须是静态方法,一般使用getInstance这个名称);当咱们调用这个方法时,若是类持有的引用不为空就返回这个引用,若是类保持的引用为空就建立该类的实例并将实例的引用赋予该类保持的引用;同时咱们还将该类的构造函数定义为私有方法,这样其余处的代码就没法经过调用该类的构造函数来实例化该类的对象,只有经过该类提供的静态方法来获得该类的惟一实例。安全
咱们将建立一个 SingleObject 类。SingleObject 类有它的私有构造函数和自己的一个静态实例。多线程
SingleObject 类提供了一个静态方法,供外界获取它的静态实例。SingletonPatternDemo,咱们的演示类使用 SingleObject 类来获取SingleObject 对象。函数
①建立一个 Singleton 类。性能
SingleObject.java优化
public class SingleObject { //建立 SingleObject 的一个对象 private static SingleObject instance = new SingleObject(); //让构造函数为 private,这样该类就不会被实例化 private SingleObject(){} //获取惟一可用的对象 public static SingleObject getInstance(){ return instance; } public void showMessage(){ System.out.println("Hello World!"); } }
②从 singleton 类获取惟一的对象。
SingletonPatternDemo.java
public class SingletonPatternDemo { public static void main(String[] args) { //不合法的构造函数 //编译时错误:构造函数 SingleObject() 是不可见的 //SingleObject object = new SingleObject(); //获取惟一可用的对象 SingleObject object = SingleObject.getInstance(); //显示消息 object.showMessage(); } }
输出结果:
Hello World!
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。由于没有加锁 synchronized,因此严格意义上它并不算单例模式。
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
这段代码简单明了,并且使用了懒加载( lazy loading )模式,可是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会建立多个实例。也就是说在多线程下不能正常工做。
接下来介绍的几种实现方式都支持多线程,可是在性能上有所差别。
为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。
描述:这种方式具有很好的 lazy loading,可以在多线程中很好的工做,可是,效率很低,99% 状况下不须要同步。
优势:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
虽然作到了线程安全,而且解决了多实例的问题,可是它并不高效。由于在任什么时候候只能有一个线程调用 getInstance() 方法。可是同步操做只须要在第一次调用时才被须要(即if语句中判断 instance 为null时才调用,不为null时是直接返回instance的),即第一次建立单例实例对象时。这就引出了双重检验锁。
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,由于会有两次检查 instance == null
,一次是在同步块外,一次是在同步块内。为何在同步块内还要再检验一次?由于可能会有多个线程一块儿进入同步块外的 if,若是在同步块内不进行二次检验的话就会生成多个实例了。
描述:这种方式采用双锁机制,安全且在多线程状况下能保持高性能。
getInstance() 的性能对应用程序很关键。
public class Singleton { private volatile static Singleton singleton;//声明成 volatile private Singleton() {} public static Singleton getSingleton() { if (instance == null) { // Single Checked synchronized (Singleton.class) { if (instance == null) { // Double Checked instance = new Singleton(); } } } return instance; } }
注意上面使用了volatile 关键字。
若是去掉volatile 关键字,它是有问题。主要在于instance = new Singleton()
这句,这并不是是一个原子操做,事实上在 JVM 中这句话大概作了下面 3 件事情。
可是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序多是 1-2-3 也多是 1-3-2。若是是后者,则在 3 执行完毕、2 未执行以前,被线程二抢占了,这时 instance 已是非 null 了(但却没有初始化),因此线程二会直接返回 instance,而后使用,而后瓜熟蒂落地报错。
因此咱们只须要将 instance 变量声明成 volatile 就能够了。
有些人认为使用 volatile 的缘由是可见性,也就是能够保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但实际上是不对的。使用 volatile 的主要缘由是其另外一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操做后面会有一个内存屏障(生成的汇编代码上),读操做不会被重排序到内存屏障以前。好比上面的例子,取操做必须在执行完 1-2-3 以后或者 1-3-2 以后,不存在执行到 1-3 而后取到值的状况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操做都先行发生于后面对这个变量的读操做(这里的“后面”是时间上的前后顺序)。
可是特别注意在 Java 5 之前的版本使用了 volatile 的双检锁仍是有问题的。其缘由是 Java 5 之前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能彻底避免重排序,主要是 volatile 变量先后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,因此在这以后才能够放心使用 volatile。
相信你不会喜欢这种复杂又隐含问题的方式,固然咱们有更好的实现线程安全的单例模式的办法。
饿汉法就是在第一次引用该类的时候就建立对象实例,而无论实际是否须要建立。
描述:这种方式比较经常使用,但容易产生垃圾对象。
优势:没有加锁,执行效率会提升。
缺点:类加载时就初始化,浪费内存。
它基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然致使类装载的缘由有不少种,在单例模式中大多数都是调用 getInstance 方法, 可是也不能肯定有其余的方式(或者其余的静态方法)致使类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }
这种写法若是完美的话,就不必在啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即便客户端没有调用 getInstance()方法。饿汉式的建立方式在一些场景中将没法使用:譬如 Singleton 实例的建立是依赖参数或者配置文件的,在 getInstance() 以前必须调用某个方法设置参数给它,那样这种单例写法就没法使用了。
描述:这种方式能达到双检锁方式同样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的状况,双检锁方式可在实例域须要延迟初始化时使用。
这种方式一样利用了 classloder 机制来保证初始化 instance 时只有一个线程,它跟第 4 种方式不一样的是:第 4 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不必定被初始化。由于 SingletonHolder 类没有被主动使用,只有显示经过调用 getInstance 方法时,才会显示装载 SingletonHolder 类,从而实例化 instance。想象一下,若是实例化 instance 很消耗资源,因此想让它延迟加载,另一方面,又不但愿在 Singleton 类加载时就实例化,由于不能确保 Singleton 类还可能在其余的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 4 种方式就显得很合理。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
这种写法仍然使用JVM自己机制保证了线程安全问题;因为 SingletonHolder 是私有的,除了 getInstance() 以外没有办法访问它,所以它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
描述:这种实现方式尚未被普遍采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止屡次实例化。
这种方式是 Effective Java 做者 Josh Bloch 提倡的方式,它不只能避免多线程同步问题,并且还自动支持序列化机制,防止反序列化从新建立新的对象,绝对防止屡次实例化。不过,因为 JDK1.5 以后才加入 enum 特性,用这种方式写难免让人感受生疏,在实际工做中,也不多用。
不能经过 reflection attack 来调用私有构造方法。
public enum Singleton { INSTANCE; public void whateverMethod() { } }
单例的特色:外界没法经过构造器来建立对象,该类必须提供一个静态方法向外界提供该类的惟一实例。
实现一个单例有两点注意事项,①将构造器私有,不容许外界经过构造器建立对象;②经过公开的静态方法向外界返回类的惟一实例。
通常来讲,单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。上述所说都是线程安全的实现,文章开头给出的第1种方法不算正确的写法。
经验之谈:通常状况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 4种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。若是涉及到反序列化建立对象时,能够尝试使用第 6 种枚举方式。若是有其余特殊的需求,能够考虑使用第 3 种双检锁方式。
相关文档连接:
一、单例模式