【译自:http://www.journaldev.com/1377/java-singleton-design-pattern-best-practices-with-examples】
单例是设计模式中提到的模式之一,它属于创建模式的类别。从它的定义来看,这是一种非常简单的模式,但是当具体实现时,会发现它有很多需要留意的方面。关于单例的实现方法在开发人员中已经产生过很多讨论和争议。这里我们会学到关于单例的的一些准则,不同的实现方法和一些使用上的最佳实践。
单例模式
单例模式限制了类的实例化,确保只有一个类的实例存在于Java虚拟机中。这个单例类必须提供一个全局的访问点去得到这个类的唯一实例。单例通常用在例如日志、驱动、缓存和线程池等地方;另外它也可以用在其他的一些设计模式中,例如 Abstract Factory、 Builder、 Prototype、Facade 等。在核心Java类中,有很多单例的类,像 java.lang.Runtime
,java.awt.Desktop
等。
Java的单例模式
有很多实现单例模式的方法,但是他们都有一些共同的、需要关注的概念:
- 私有的构造函数:限制其他类对它的实例化
- 私有的静态变量:用于持有唯一的当前类的实例
- 公开的静态方法 :用于返回当前类的唯一实现,这个方法也是提供给外部的全局访问点
接下来的部分,我们会学到几种不同的实现单例的方式和相应的需要关注的要求:
静态变量初始化
这种方式下,单例的实例在类被加载的时候就被初始化,这也是创建单例的最简单的方式。缺点就是即使这个实例没有被用到,也被创建了。
下面是这种实现方式的代码示例:
package com.journaldev.singleton; public class EagerInitializedSingleton { private static final EagerInitializedSingleton instance = new EagerInitializedSingleton(); //private constructor to avoid client applications to use constructor private EagerInitializedSingleton(){} public static EagerInitializedSingleton getInstance(){ return instance; } }
如果你的单例没有占用多少资源,那么可以选择这种实现方式。然后大多数情况下,单例都是针对资源而创建的,例如文件系统,数据库连接等等,所以我们应该避免对它的实例化,除非调用端调用getInstance()。方法;另外这种实现方式没有提供任务异常处理逻辑。
静态初始化块
静态初始化块的实现方式类似于上面的静态变量初始化,除了一点:静态初始化块可以提供异常处理:
package com.journaldev.singleton; public class StaticBlockSingleton { private static StaticBlockSingleton instance; private StaticBlockSingleton(){} //static block initialization for exception handling static{ try{ instance = new StaticBlockSingleton(); }catch(Exception e){ throw new RuntimeException("Exception occured in creating singleton instance"); } } public static StaticBlockSingleton getInstance(){ return instance; } }
静态变量和静态块都是在变量在使用之前就被创建了,因此不是一个好的最佳实践方式。在接下来的片段里,我们会学到如果延迟创建单例的实例。
延迟初始化
延迟初始化是通过使用一个全局访问的方法去创建单例的实例。例如下面的代码:
package com.journaldev.singleton; public class LazyInitializedSingleton { private static LazyInitializedSingleton instance; private LazyInitializedSingleton(){} public static LazyInitializedSingleton getInstance(){ if(instance == null){ instance = new LazyInitializedSingleton(); } return instance; } }
上面的代码在单线程的情况下工作的很好,不过在多线程环境中,如果有多个有线程同时访问,则可能存在问题,单例模式可能被破坏,不同的线程将得到不同的单例的实现。接下来我们看一种线程安全的实现方式。
线程安全的单例
最简单的创建线程安全的单例就是使得全局访问方法是同步的,这样一次只有一个线程可以执行这个方法。下面代码展示了这一点:
package com.journaldev.singleton; public class ThreadSafeSingleton { private static ThreadSafeSingleton instance; private ThreadSafeSingleton(){} public static synchronized ThreadSafeSingleton getInstance(){ if(instance == null){ instance = new ThreadSafeSingleton(); } return instance; } }
上面的实现方式工作的很好,并且提供了线程安全的保障,但是它降低了性能,因为同步上的开销,同步应该只需要在一开始创建实例的时候用到。双重锁定可以用来避免每次访问的这些额外的开销,在这种实现方式下,同步块被包含在一个附加的检测中,以保证只有一个实例被创建,以下是代码实现:
public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){ if(instance == null){ synchronized (ThreadSafeSingleton.class) { if(instance == null){ instance = new ThreadSafeSingleton(); } } } return instance; }
Read: Thread Safe Singleton Class
BillPugh单例实现方式
在Java5之前,Java内存模型有很多问题,使得上面的双重锁定实现方式在某些情况下,例如很多的线程同时试图去访问单例实例,会失效。因此Bill Pugh提出了另一种不同的实现方式,通过使用内部静态助手类来实现。如下:
package com.journaldev.singleton; public class BillPughSingleton { private BillPughSingleton(){} private static class SingletonHelper{ private static final BillPughSingleton INSTANCE = new BillPughSingleton(); } public static BillPughSingleton getInstance(){ return SingletonHelper.INSTANCE; } }
注意:私有的、内部的、静态的类包含了单例的实例。当单例的类被加载时,这个内部的私有的类并没有被加载,只有当调用getInstance()方法的时候,这个类才会加载,然后创建单例的实例。这种实现方式被广泛的应用,并且不要求同步。我在很多工程都都使用这种方法,它很容易被理解。
Read: Java Nested Classes
反射破坏单例
反射能够破坏以上所有的实现方式:
package com.journaldev.singleton; import java.lang.reflect.Constructor; public class ReflectionSingletonTest { public static void main(String[] args) { EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance(); EagerInitializedSingleton instanceTwo = null; try { Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors(); for (Constructor constructor : constructors) { //Below code will destroy the singleton pattern constructor.setAccessible(true); instanceTwo = (EagerInitializedSingleton) constructor.newInstance(); break; } } catch (Exception e) { e.printStackTrace(); } System.out.println(instanceOne.hashCode()); System.out.println(instanceTwo.hashCode()); } }
当运行上面的测试类时,会发现两个实例的hashCode不一样,这证明单例被破坏了。反射很强大,并且用在很多的框架中,例如Spring, Hibernate等,请查看 Java Reflection Tutorial.
枚举单例
为了克服反射的问题,Joshua Bloch建议使用枚举去实现单例模式,因为Java确保枚举值只会被初始化一次,因为Java枚举是全局访问的,因此也是一个单例。不过缺点就是枚举不够灵活,并且不允许延迟加载。
package com.journaldev.singleton; public enum EnumSingleton { INSTANCE; public static void doSomething(){ //do something } }
Read: Java Enum
系列化和单例
有时在分布式的系统中,需要在单例上实现系列化,这样就可以把它的状态存储在文件系统上,然后在稍后的某个点将它取回,例如:
package com.journaldev.singleton; import java.io.Serializable; public class SerializedSingleton implements Serializable{ private static final long serialVersionUID = -7604766932017737115L; private SerializedSingleton(){} private static class SingletonHelper{ private static final SerializedSingleton instance = new SerializedSingleton(); } public static SerializedSingleton getInstance(){ return SingletonHelper.instance; } }
系列化一个单例的问题在于任何时候,当反系列化时,都会创建一个新的实例,例如:
package com.journaldev.singleton; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; public class SingletonSerializedTest { public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { SerializedSingleton instanceOne = SerializedSingleton.getInstance(); ObjectOutput out = new ObjectOutputStream(new FileOutputStream( "filename.ser")); out.writeObject(instanceOne); out.close(); //deserailize from file to object ObjectInput in = new ObjectInputStream(new FileInputStream( "filename.ser")); SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject(); in.close(); System.out.println("instanceOne hashCode="+instanceOne.hashCode()); System.out.println("instanceTwo hashCode="+instanceTwo.hashCode()); } }
以下是上面程序的输出:
instanceOne hashCode=2011117821 instanceTwo hashCode=109647522
因此它也破坏了单例模式,这种情况下,我们需要实现readResolve()方法以阻止实例的创建:
protected Object readResolve() { return getInstance(); }
然后就可以验证两个实例的hashCode相同。