设计模式一直流行于程序员之间,本文讨论被许多人认为最简单但最有争议的设计模式 —— 单例模式
在软件工程中,设计模式描述了如何解决重复出现的设计问题,以设计出灵活、可复用的面向对象的应用程序。设计模式一共有 23 种,可以将它们分为三个不同的类别 —— 创建型、结构型和行为型。
创建型设计模式是处理对象创建机制的模式,试图以适合的方式创建对象。
对象创建的基本形式可能会导致设计问题或增加设计的复杂性。创建型设计模式通过某种方式控制对象的创建来解决此问题。
结构型设计模式处理类和对象的组成。这类模式使我们将对象和类组装为更大的结构,同时保持结构的高效和灵活。
行为型设计模式讨论对象的通信以及它们之间如何交互。
我们对设计模式和其类型进行了概述,接下来我们重点介绍单例设计模式。
单例模式提供了控制程序中允许创建的实例数量的能力,同时确保程序中有一个单例的全局访问点。
单例设计模式可以通过多种方式实现。每一种都有其自身的优点和局限性,我们可以通过以下几种方式实现单例模式:
本节我们将讨论实现单例模式的各种方法。
public class EagerInitialization { private static EagerInitialization INSTANCE = new EagerInitialization(); private EagerInitialization() { } public static EagerInitialization getInstance() { return INSTANCE; } }
import java.util.Objects; public class LazyInit { private static LazyInit INSTANCE = null; private LazyInit() { } public static LazyInit getInstance() { if (null == INSTANCE) { synchronized (LazyInit.class) { INSTANCE = new LazyInit(); } } return INSTANCE; } }
import java.util.Objects; public class LazyInitialization { private static LazyInitialization INSTANCE = null; private LazyInitialization() { } public synchronized static LazyInitialization getInstance() { if (null == INSTANCE) { INSTANCE = new LazyInitialization(); } return INSTANCE; } }
import java.util.Objects; public class DoubleCheckSingleton { private static DoubleCheckSingleton INSTANCE; private DoubleCheckSingleton(){} public static DoubleCheckSingleton getInstance() { if(null == INSTANCE){ synchronized (DoubleCheckSingleton.class) { if(null == INSTANCE){ INSTANCE = new DoubleCheckSingleton(); } } } return INSTANCE; } }
说明一下为什么这种方法在多线程常见下可能存在问题:
INSTANCE = new DoubleCheckSingleton();
这句代码,实际上可以分解成以下三个步骤:
但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:
在多线程中就会出现第二个线程判断对象不为空,但此时对象还未初始化的情况。
import java.util.Objects; public class DoubleCheckSingleton { private volatile static DoubleCheckSingleton INSTANCE; private DoubleCheckSingleton(){} public static DoubleCheckSingleton getInstance() { if(null == INSTANCE){ synchronized (DoubleCheckSingleton.class) { if(null == INSTANCE){ INSTANCE = new DoubleCheckSingleton(); } } } return INSTANCE; } }
为了解决上述问题,需要加入关键字 volatile
。使用了 volatile
关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。
public enum Singleton { INSTANCE; }
在所有的单例实现中(枚举方法除外),我们通过提供私有构造函数来确保单例。但是,可以通过 反射 来访问私有构造函数,反射是在运行时检查或修改类的运行时行为的过程。
让我们演示如何通过反射访问单例:
import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public class SingletonWithReflection { public static void main(String[] args) { EagerInitializedSingleton firstSingletonInstance = EagerInitializedSingleton.getInstance(); EagerInitializedSingleton secondSingletonInstance = null; try{ Class<EagerInitializedSingleton> clazz = EagerInitializedSingleton.class; Constructor<EagerInitializedSingleton> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); secondSingletonInstance = constructor.newInstance(); } catch(NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e){ e.printStackTrace(); } System.out.println("Instance 1 hashcode: "+firstSingletonInstance.hashCode()); System.out.println("Instance 2 hashcode: "+secondSingletonInstance.hashCode()); } }
上边代码输出如下:
Instance 1 hashcode: 21049288 Instance 2 hashcode: 24354066
如果单例对象已经初始化,则可以通过禁止对构造函数的访问来防止通过反射访问单例类。如果在对象初始化之后调用构造函数,可以通过抛出异常的方式来实现。
public final class EagerInitializedSingleton { private static final EagerInitializedSingleton INSTANCE = new EagerInitializedSingleton(); private EagerInitializedSingleton(){ if(Objects.nonNull(INSTANCE)){ throw new RuntimeException("This class can only be access through getInstance()"); } } public static EagerInitializedSingleton getInstance(){ return INSTANCE; } }
在分布式应用程序中,有时我们会序列化一个对象,以将对象状态保存在持久化存储中,并用以之后的检索。保存对象状态的过程称为 序列化 ,而检索操作称为 反序列化 。
如果单例没有被正确实现,那么可能出现一个单例对象有两个实例的情况。
让我们看看如何出现这种情况:
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class SingletonWithSerialization { public static void main(String[] args) { EagerInitializedSingleton firstSingletonInstance = EagerInitializedSingleton.getInstance(); EagerInitializedSingleton secondSingletonInstance = null; ObjectOutputStream outputStream = null; ObjectInputStream inputStream = null; try{ // 将对象状态保存到文件中 outputStream = new ObjectOutputStream(new FileOutputStream("FirstSingletonInstance.ser")); outputStream.writeObject(firstSingletonInstance); outputStream.close(); // 从文件中检索对象状态 inputStream = new ObjectInputStream(new FileInputStream("FirstSingletonInstance.ser")); secondSingletonInstance = (EagerInitializedSingleton) inputStream.readObject(); inputStream.close(); } catch(Exception e){ e.printStackTrace(); } System.out.println("FirstSingletonInstance hashcode: "+firstSingletonInstance.hashCode()); System.out.println("SecondSingletonInstance hashcode: "+secondSingletonInstance.hashCode()); } }
以上代码输出如下:
FirstSingletonInstance hashcode: 23090923 SecondSingletonInstance hashcode: 19586392
这说明现在有两个单例实例。
注意,单例类必须实现 Serializable
接口才能序列化实例。
为了避免序列化产生多个实例,我们可以在单例类中实现 readResolve()
方法。这个方法将会替换从流中读取的对象。实现代码如下:
import java.io.Serializable; import java.util.Objects; public class EagerInitializedSingleton implements Serializable { private static final long serialVersionUID = 1L; private static final EagerInitializedSingleton INSTANCE = new EagerInitializedSingleton(); private EagerInitializedSingleton(){ if(Objects.nonNull(INSTANCE)){ throw new RuntimeException("This class can only be access through getInstance()"); } } public static EagerInitializedSingleton getInstance(){ return INSTANCE; } protected Object readResolve(){ return getInstance(); } }
再次执行 SingletonWithSerialization
输出如下:
FirstSingletonInstance hashcode: 24336889 SecondSingletonInstance hashcode: 24336889
Java API 中有很多类是用单例设计模式设计的:
java.lang.Runtime#getRuntime() java.awt.Desktop#getDesktop() java.lang.System#getSecurityManager()
单例模式是最重要和最常用的设计模式之一。尽管很多人批评它是一种反模式,并且有很多实现时的注意事项,但在实际生活中有很多使用这种模式的示例。本文尝试介绍了常见的单例设计和与之相关的缺陷。