介绍
Java 单例模式是《四人组设计模式》之一,属于创建型设计模式类别。从定义上看,它似乎是一种简单直接的设计模式,但在实现时,它涉及到许多考虑因素。
在本文中,我们将学习单例设计模式的原则,探讨实现单例设计模式的不同方法,并介绍一些最佳实践。
单例模式原则
- 单例模式限制了类的实例化,并确保在 Java 虚拟机中只存在一个类的实例。
- 单例类必须提供一个全局访问点来获取类的实例。
- 单例模式用于日志记录、驱动对象、缓存和线程池。
- 单例设计模式也被用于其他设计模式,比如抽象工厂,建造者,原型,外观等。
- 单例设计模式还被用于核心Java类(例如,
java.lang.Runtime
,java.awt.Desktop
)。
Java单例模式实现
为了实现单例模式,我们有不同的方法,但它们都有以下共同的概念。
- 私有构造函数以限制其他类实例化该类。
- 同一类的私有静态变量,是该类的唯一实例。
- 公共静态方法返回该类的实例,这是外界获取单例类实例的全局访问点。
在接下来的部分中,我们将学习单例模式实现的不同方法以及实现中的设计考虑事项。
1. 渴望初始化
在渴望初始化中,单例类的实例在类加载时创建。渴望初始化的缺点是即使客户端应用程序可能不使用它,该方法也会被创建。以下是静态初始化单例类的实现:
package com.journaldev.singleton;
public class EagerInitializedSingleton {
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
// 私有构造函数以避免客户端应用程序使用构造函数
private EagerInitializedSingleton(){}
public static EagerInitializedSingleton getInstance() {
return instance;
}
}
如果您的单例类没有使用大量资源,这是要使用的方法。但在大多数情况下,单例类是为文件系统、数据库连接等资源创建的。除非客户端调用 getInstance
方法,否则我们应该避免实例化。此外,该方法不提供任何异常处理选项。
2. 静态块初始化
静态块初始化实现类似于急切初始化,唯一的区别是类的实例是在提供了选项的静态块中创建的,用于异常处理。
package com.journaldev.singleton;
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton(){}
// 用于异常处理的静态块初始化
static {
try {
instance = new StaticBlockSingleton();
} catch (Exception e) {
throw new RuntimeException("Exception occurred in creating singleton instance");
}
}
public static StaticBlockSingleton getInstance() {
return instance;
}
}
急切初始化和静态块初始化都会在实例被使用之前创建实例,这不是最佳实践。
3. 懒加载
使用懒加载方法来实现单例模式会在全局访问方法中创建实例。以下是使用此方法创建单例类的示例代码:
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;
}
}
在单线程环境下,上述实现方式是有效的,但是在多线程系统中,如果多个线程同时处于if
条件内部,就会出现问题。这会破坏单例模式,并且两个线程将获得单例类的不同实例。在接下来的部分中,我们将看到创建线程安全单例类的不同方法。
4. 线程安全的单例模式
A simple way to create a thread-safe singleton class is to make the global access method synchronized so that only one thread can execute this method at a time. Here is a general implementation of this approach:
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;
}
}
前面的实现可以很好地提供线程安全性,但由于与同步方法相关的成本,它会降低性能,尽管我们只需要为可能创建单独实例的前几个线程使用它。为了避免每次都有这额外开销,采用了双重检查锁定原则。在这种方法中,使用同步块放在if
条件内部,并额外检查以确保只创建一个单例类的实例。以下代码片段提供了双重检查锁定的实现:
public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
继续学习,了解线程安全的单例类。
5. Bill Pugh 单例模式实现
在Java 5之前,Java内存模型存在许多问题,先前的方法在某些场景中失败,即当太多线程同时尝试获取单例类的实例时。因此,Bill Pugh提出了一种不同的方法,使用内部静态辅助类创建单例类。以下是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;
}
}
请注意私有的内部静态类,其中包含单例类的实例。当单例类被加载时,SingletonHelper
类不会加载到内存中,只有当有人调用getInstance()
方法时,该类才会加载并创建单例类实例。这是单例类最广泛使用的方法,因为它不需要同步。
6. 使用反射破坏单例模式
反射可以用来破坏先前的所有单例实现方法。以下是一个示例类:
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) {
// 这段代码将破坏单例模式
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反射教程。
7. 枚举单例
为了克服这种情况,Joshua Bloch建议使用enum
来实现单例设计模式,因为Java确保任何enum
值在Java程序中仅实例化一次。由于Java Enum值是全局可访问的,所以单例也是如此。缺点是enum
类型有些不灵活(例如,它不允许延迟初始化)。
package com.journaldev.singleton;
public enum EnumSingleton {
INSTANCE;
public static void doSomething() {
// 做一些事情
}
}
8. 序列化和单例
有时在分布式系统中,我们需要在单例类中实现Serializable
接口,以便可以将其状态存储在文件系统中,并在以后的某个时间点检索它。这是一个实现了Serializable
接口的小型单例类:
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();
// 从文件反序列化为对象
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());
}
}
该代码产生以下输出:
OutputinstanceOne hashCode=2011117821
instanceTwo hashCode=109647522
因此它破坏了单例模式。要克服这种情况,我们只需要提供readResolve()
方法的实现。
protected Object readResolve() {
return getInstance();
}
之后,您将注意到测试程序中两个实例的hashCode
是相同的。
结论
本文介绍了单例设计模式。
通过更多的Java教程继续学习。