最近在看 『Refacting to Patterns』,结合之前看的 Test Driven-Practical TDD and Acceptance TDD for Java Developers 提到的测试驱动开发(TDD)。虽然早些时候,我已经有对一些设计模式有一定了解,但只有对创建型的设计模型有所应用,比如Singleton、Builder、Factory、AbstractFactory。而其他的行为型和结构型的设计模型,只有在阅读一些源代码的时候偶尔才有看到,如 Java 集合框架中使用的 Iterator 模式,然而自己却很少使用。
为了更好地执行 TDD 过程中的重构,以及看懂优秀项目的源代码,我觉得我有必要开始重新更深入地学习设计模式。
单例模式,即 Gof (Gangs of Four)设计模式中创建型(Creational)设计模式之一。用于限制类的实例化,它只允许当前 JVM 执行上下文中只能拥有限制类的一个实例。从定义来看,似乎它的实现应该比较简单,但是在实际实现时,它有许多需要注意的点。在 Java 中,光它实现方式现已有许多种。因此,本文将介绍单例模式的实现方法,简单分析每一种方法的优缺点,最后,再说明应用单例模式时可能遇到的问题以及相应解决办法。
1. 单例模式实现方式 1.1 饿汉式在饿汉式的实现中,单例类的实例在类加载的时候创建,即类初始化对静态成员变量初始化时,创建该类的实例。这是最简单的实现方式,但却存在明显的缺点,它不管应用程序是否需要该类的实例,就创建相应的实例。
1 2 3 4 5 6 7 8 9 10 public class EagerSingleton { private static final EagerSingleton INSTANCE = new EagerSingleton(); private EagerSingleton () {} public static EagerSingleton getInstance () { return INSTANCE; } }
如果该类的实例不是一个大对象时,若我们容忍应用程序在运行时,该对象的实例存在但不使用,则该实现方式将是最佳方案。
1.2 静态代码块与饿汉式实现相似,同样地,类加载时,在初始化(initialization)阶段除了对静态成员变量进行初始化外,还会执行静态代码块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class StaticBlockSingleton { private static StaticBlockSingleton instance; private StaticBlockSingleton () {} static { try { instance = new StaticBlockSingleton(); } catch (Exception e) { throw new RuntimeException("Exception occured in creating singleton instance!" ); } } public static StaticBlockSingleton getInstance () { return instance; } }
这种方式和饿汉式存在相同的问题,它不管应用程序是否需要该类的实例,在类加载时,相应的实例就会被创建。
1.3 懒汉式不同于饿汉式,懒汉式实现方式将实例的创建延迟到 getInstance
方法调用时。
1 2 3 4 5 6 7 8 9 10 11 12 public class LazySingleton { private static LazySingleton instance = null ; private LazySingleton () {} public static LazySingleton getInstance () { if (instance == null ) { instance = new LazySingleton(); } return instance; } }
首先,在实例被真正需要才创建是一种非常好的策略,当创建该类的实例是一个较昂贵的操作时,它比饿汉式表现更好。上面的代码可以在单线程环境下正常执行,但在多线程环境下,单例模式将被破坏,可能会有多个线程同时进入 if
代码块,导致多个实例被创建出来。为此,我们可能还需要线程安全的实现方式。
1.4 线程安全式 1.4.1 synchronized实现线程安全的最简单方式是,在懒汉式实现的基础上,对 getInstance
方法使用 synchronized
关键字进行修饰即可。这样可以确保某一时间只有一个线程可以执行该方法,其他的线程需要等待该执行线程释放该类的锁对象。
1 2 3 4 5 6 7 8 9 10 11 12 public class SynchronizedSingleton { private static volatile SynchronizedSingleton instance; private SynchronizedSingleton () {} public static synchronized SynchronizedSingleton getInstance () { if (instance == null ) { instance = new SynchronizedSingleton(); } return instance; } }
1.4.2 double-checking1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class DoubleCheckingSingleton { private static volatile DoubleCheckingSingleton instance; private DoubleCheckingSingleton () {} public static DoubleCheckingSingleton getInstance () { if (instance == null ) { synchronized (DoubleCheckingSingleton.class) { if (instance == null ) { instance = new DoubleCheckingSingleton(); } } } return instance; } }
双重检查机制——在使用 synchronized
关键字同步创建单例的代码块的时候,需要在同步的代码块再次使用 if
来判断实例是否已经创建。因为可能存在多个线程进入第一个 if
块,然后阻塞等待,如果没有在 synchronized
块中多设置一个 if
来判断实例是否创建,此时将破坏单例模型的原则。
注意上面的两个实现都应该使用 volatile
关键字来修饰单例成员(避免其他线程在实例创建之前引用该变量,而在之后运行时出现 NPE
异常)。
1.5 静态内部类——Bill Pugh Singleton1 2 3 4 5 6 7 8 9 10 11 public class BillPughSingleton { private static SingletonHolder { private static final BillPughSingleton INSTANCE = new BillPughSingleton(); } private BillPughSingleton () {} public static BillPughSingleton getInstance () { return SingletonHolder.INSTANCE; } }
该实现方式主要利用 Java 的加载和初始化过程,loading -> Linking -> Initialization。其中初始化阶段主要是初始化静态成员变量并执行静态代码块。因此,BillPughSingleton
只有在执行 getInstance()
方法时,才会将 SingletonHolder
加载到方法区中(method area),再对其静态成员变量进行初始化。
1.6 枚举-Enum Singleton1 2 3 4 5 6 7 public Enum EnumSingleton { INSTANCE; public void someMethod (String param) { } }
根据 Java 对 Enum
的实现文档,Enum
隐式地保证了线程安全以及单例。因此,使用枚举来实现单例模式也是一个不错的选择。
2. 序列化问题虽然上面我们已经了解了许多单例模式的实现方式,但在实际使用的时候,我们有可能会遇到一些问题。例如:在序列化和反序列化时就有可能打破单例模式。我们看下面一个例子。
DemoSingleton.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.io.Serializable;public DemoSingleton implements Serializable { private static class SingletonHolder { private final static DemoSingleton INSTANCE = new DemoSingleton(); } public static DemoSingleton getInstance () { return SingletonHolder.INSTANCE; } private DemoSingleton () {} private int id; public void setI (int i) { this .i = i; } public int getI () { return i; } }
SerialzableProblem.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class SerialzableProblem { static DemoSingleton instanceOne = DemoSingleton.getInstance(); public static void main (String[] args) { try { ObjectOutput out = new ObjectOutputStream(new FileOutputStream("filename.ser" )); out.writeObject(instanceOne); out.close(); instanceOne.setI(20 ); ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser" )); DemoSingleton instanceTwo = (DemoSingleton) in.readObject(); in.close(); System.out.println(instanceOne.getI()); System.out.println(instanceTwo.getI()); } catch (Exception e) { e.printStackTrace(); } } } }
Output:
这意味着,JVM 环境中出现了 DemoSingleton
的多个实例。这种情况下,单例模式被打破。我们需要在 DemoSingleton
类中添加 readResolve
方法来解决找一个问题。
类在反序列化的时候会调用 readResolve
,此时返回该类的唯一单例。
3. 最佳实践最佳实践的代码模板,能解决线程安全问题和序列化反序列化可能破坏单例模式的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class BillPughSingleton implements Serializable { private static final long serialVersionUID = 1L ; private class SingletonHolder { private static final BillPughSingleton INSTANCE = new BillPughSingleton(); } private BillPughSingleton () {} public static BillPughSingleton getInstance () { return SingletonHolder.INSTANCE; } protected Object readResolve () { return SingletonHolder.INSTANCE; } }
4. 用 Java 的反射来破坏单例模式通过使用反射(Reflection)可以破坏上面的所有单例模式实现方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.lang.reflect.Constructor;public class ReflectSingletonTest { public static void main (String[] args) { BillPughSingleton instanceOne = BillPughSingleton.getInstance(); BillPughSingleton instanceTwo = null ; try { Constructor[] constructors = BillPughSingleton.class.getDeclaredConstructors(); for (Constrctor constructor : constructors) { constructor.setAccessible(true ); instanceTwo = (BillPughSingleton) constructor.newInstance(); break ; } } catch (Exception e) { e.printStackTrace(); } System.out.println(instanceOne.hashCode()); System.out.println(instanceTwo.hashCode()); } }
References
https://howtodoinjava.com/design-patterns/creational/singleton-design-pattern-in-java/ https://www.journaldev.com/1377/java-singleton-design-pattern-best-practices-examples