Java 单例设计模式的最佳实践和示例
引言
Java单例模式是四人帮设计模式之一,属于创建型设计模式。从定义上看,它似乎是一个简单直接的设计模式,但在实现时却涉及很多考虑因素。
在这篇文章中,我们将学习单例设计模式的原则,探索不同的实现单例设计模式的方式,以及一些最佳实践。
单例模式原则
- Singleton pattern restricts the instantiation of a class and ensures that only one instance of the class exists in the Java Virtual Machine.
- The singleton class must provide a global access point to get the instance of the class.
- Singleton pattern is used for logging, drivers objects, caching, and thread pool.
- Singleton design pattern is also used in other design patterns like Abstract Factory, Builder, Prototype, Facade, etc.
- Singleton design pattern is used in core Java classes also (for example, java.lang.Runtime, java.awt.Desktop).
Java 单例模式的实现方式
为了实现单例模式,我们有不同的方法,但它们都有以下共同的概念。
- Private constructor to restrict instantiation of the class from other classes.
- Private static variable of the same class that is the only instance of the class.
- Public static method that returns the instance of the class, this is the global access point for the outer world to get the instance of the singleton class.
在接下来的部分,我们将学习单例模式实现的不同方法和实现中的设计考虑事项。
1. 热切初始化
在急切初始化中,单例类的实例在类加载时创建。急切初始化的缺点是,即使客户端应用程序可能不使用该方法,该方法仍然被创建。以下是静态初始化单例类的实现方式:
package com.scdev.singleton;
public class EagerInitializedSingleton {
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
// private constructor to avoid client applications using the constructor
private EagerInitializedSingleton(){}
public static EagerInitializedSingleton getInstance() {
return instance;
}
}
如果您的单例类没有使用大量的资源,这是要采用的方法。但是在大多数情况下,单例类是为文件系统、数据库连接等资源创建的。除非客户端调用getInstance方法,我们应避免实例化。此外,此方法没有提供任何异常处理选项。
2. 静态块初始化
静态块初始化实现与急切初始化类似,只是类的实例在静态块中创建,提供了异常处理的选项。
package com.scdev.singleton;
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton(){}
// static block initialization for exception handling
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.scdev.singleton;
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton(){}
public static LazyInitializedSingleton getInstance() {
if (instance == null) {
instance = new LazyInitializedSingleton();
}
return instance;
}
}
在单线程环境下,上述的实现是有效的。但是,当涉及到多线程系统时,如果多个线程同时处于if条件中,就会出现问题。这将破坏单例模式,并且两个线程将获取不同的单例类实例。接下来的章节中,我们将看到创建线程安全单例类的不同方法。
4. 线程安全的单例模式
(Note: This is the paraphrased version of “Thread Safe Singleton” in Chinese)
创建线程安全的单例类的一种简单方式是将全局访问方法设为同步方法,从而一次只允许一个线程执行该方法。以下是这种方法的一般实现:
package com.scdev.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. 比尔普斯单例模式
在Java 5之前,Java内存模型存在许多问题,并且先前的方法在太多线程同时尝试获取单例类的实例时会失败。因此,Bill Pugh想出了一种不同的方法来创建单例类,使用内部的静态辅助类。以下是Bill Pugh单例实现的示例。
package com.scdev.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.scdev.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) {
// This code will destroy the singleton pattern
constructor.setAccessible(true);
instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
当你运行上述测试类时,你会注意到两个实例的哈希码不相同,这破坏了单例模式。反射非常强大,并且在很多框架(如Spring和Hibernate)中被广泛使用。通过Java反射教程继续学习。
7. 枚举单例
为了解决这种情况下的反射问题,Joshua Bloch建议使用枚举实现单例设计模式,因为Java确保在Java程序中任何枚举值只实例化一次。由于Java枚举值全局可访问,所以单例模式也是如此。缺点是枚举类型有些不灵活(例如,不允许延迟初始化)。
package com.scdev.singleton;
public enum EnumSingleton {
INSTANCE;
public static void doSomething() {
// do something
}
}
8. 序列化和单例模式
在分布式系统中,有时候我们需要在单例类中实现Serializable接口,以便将其状态存储在文件系统中,并在以后的时间点上检索。下面是一个实现了Serializable接口的小型单例类。
package com.scdev.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.scdev.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();
// deserialize from file to object
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());
}
}
那段代码会产生这个输出结果。
instanceOne hashCode=2011117821 instanceTwo hashCode=109647522
因此它破坏了单例模式。为了克服这种情况,我们只需要提供readResolve()方法的实现。
protected Object readResolve() {
return getInstance();
}
在此之后,您将注意到测试程序中两个实例的hashCode是相同的。
阅读有关Java序列化和Java反序列化的内容。
结论
这篇文章介绍了单例设计模式。
继续学习,参加更多的Java教程。