0%

Java 单例模式完整指南

设计模式一直流行于程序员之间,本文讨论被许多人认为最简单但最有争议的设计模式 —— 单例模式

设计模式概述

在软件工程中,设计模式描述了如何解决重复出现的设计问题,以设计出灵活、可复用的面向对象的应用程序。设计模式一共有 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. 将对象指向刚分配的内存空间

但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:

  1. 分配内存空间
  2. 将对象指向刚分配的内存空间
  3. 初始化对象

在多线程中就会出现第二个线程判断对象不为空,但此时对象还未初始化的情况。

正确的双重检查

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()

结论

单例模式是最重要和最常用的设计模式之一。尽管很多人批评它是一种反模式,并且有很多实现时的注意事项,但在实际生活中有很多使用这种模式的示例。本文尝试介绍了常见的单例设计和与之相关的缺陷。