设计模式一直流行于程序员之间,本文讨论被许多人认为最简单但最有争议的设计模式 —— 单例模式
设计模式概述 在软件工程中,设计模式描述了如何解决重复出现的设计问题,以设计出灵活、可复用的面向对象的应用程序。设计模式一共有 23 种,可以将它们分为三个不同的类别 —— 创建型、结构型和行为型。
创建型设计模式 创建型设计模式是处理对象创建机制的模式,试图以适合的方式创建对象。
对象创建的基本形式可能会导致设计问题或增加设计的复杂性。创建型设计模式通过某种方式控制对象的创建来解决此问题。
结构型设计模式 结构型设计模式处理类和对象的组成。这类模式使我们将对象和类组装为更大的结构,同时保持结构的高效和灵活。
行为型设计模式 行为型设计模式讨论对象的通信以及它们之间如何交互。
单例设计模式 我们对设计模式和其类型进行了概述,接下来我们重点介绍单例设计模式。
单例模式提供了控制程序中允许创建的实例数量的能力,同时确保程序中有一个单例的全局访问点。
优点
对单个实例的访问控制
减少对象数量
允许完善操作和表示
缺点
被很多程序员视为反模式
在可能不需要单例的情况下被误用
实现 单例设计模式可以通过多种方式实现。每一种都有其自身的优点和局限性,我们可以通过以下几种方式实现单例模式:
预先初始化(Eager Initialization)
静态块预初始化
延迟初始化(Lazy Initialization)
线程安全的延迟初始化
双重检查的延迟初始化
单个实例的枚举
实现单例 本节我们将讨论实现单例模式的各种方法。
预先初始化(Eager Initialization)
用预先初始化方法,对象在创建之前就已被初始化
由于预先初始化,所以可能会出现程序并不需要的情况下初始化
如果单例类很简单并且不需要太多资源,这个方法会很有用。
1 2 3 4 5 6 7 8 9 10 11 12 public class EagerInitialization { private static EagerInitialization INSTANCE = new EagerInitialization(); private EagerInitialization() { } public static EagerInitialization getInstance() { return INSTANCE; } }
静态块预初始化
在前面讨论的预先初始化方法中,没有提供任何异常处理逻辑
在此实现中,对象是在静态块中创建的,因此在对象初始化时可以进行异常处理
这种方法和预先初始化有同样的问题:即使程序可能不使用对象,对象也会被提前创建出来
延迟初始化(Lazy Initialization)
按需创建对象
与预先初始化不同,延迟初始化会在需要时创建对象
此实现不是线程安全的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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; } }
线程安全的延迟初始化
添加了用以处理多线程的同步方案
因为每次调用都需要进行方法级同步,而过度的同步会降低性能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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; } }
双重检查的延迟初始化
解决方法级同步的问题
为对象可为空性执行双重检查
尽管这种方法似乎可以完美的工作,但是在多线程场景下不太适用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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();
这句代码,实际上可以分解成以下三个步骤:
分配内存空间
初始化对象
将对象指向刚分配的内存空间
但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:
分配内存空间
将对象指向刚分配的内存空间
初始化对象
在多线程中就会出现第二个线程判断对象不为空,但此时对象还未初始化的情况。
正确的双重检查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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)操作之前。
单个实例的枚举
使用 Java 枚举类型创建单例
此方法为处理反射、序列化和多线程场景提供了本地支持
枚举类型有些不灵活
1 2 3 public enum Singleton { INSTANCE; }
保护单例 使单例不受反射访问的影响 在所有的单例实现中(枚举方法除外),我们通过提供私有构造函数来确保单例。但是,可以通过反射 来访问私有构造函数,反射是在运行时检查或修改类的运行时行为的过程。 让我们演示如何通过反射访问单例:
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 26 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()); } }
上边代码输出如下:
1 2 Instance 1 hashcode: 21049288 Instance 2 hashcode: 24354066
解决: 如果单例对象已经初始化,则可以通过禁止对构造函数的访问来防止通过反射访问单例类。如果在对象初始化之后调用构造函数,可以通过抛出异常的方式来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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; } }
使单例在序列化时安全 在分布式应用程序中,有时我们会序列化一个对象,以将对象状态保存在持久化存储中,并用以之后的检索。保存对象状态的过程称为序列化 ,而检索操作称为反序列化 。
如果单例没有被正确实现,那么可能出现一个单例对象有两个实例的情况。
让我们看看如何出现这种情况:
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 26 27 28 29 30 31 32 33 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()); } }
以上代码输出如下:
1 2 FirstSingletonInstance hashcode: 23090923 SecondSingletonInstance hashcode: 19586392
这说明现在有两个单例实例。
注意,单例类必须实现 Serializable
接口才能序列化实例。
为了避免序列化产生多个实例,我们可以在单例类中实现 readResolve()
方法。这个方法将会替换从流中读取的对象。实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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
输出如下:
1 2 FirstSingletonInstance hashcode: 24336889 SecondSingletonInstance hashcode: 24336889
Java API 中的单例示例 Java API 中有很多类是用单例设计模式设计的:
1 2 3 java.lang.Runtime#getRuntime() java.awt.Desktop#getDesktop() java.lang.System#getSecurityManager()
结论 单例模式是最重要和最常用的设计模式之一。尽管很多人批评它是一种反模式,并且有很多实现时的注意事项,但在实际生活中有很多使用这种模式的示例。本文尝试介绍了常见的单例设计和与之相关的缺陷。