企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# Java 单例模式介绍 > 原文: [https://howtodoinjava.com/design-patterns/creational/singleton-design-pattern-in-java/](https://howtodoinjava.com/design-patterns/creational/singleton-design-pattern-in-java/) **单例模式**是一种设计解决方案,其中应用希望在所有可能的情况下都具有一个且只有一个任何类的实例,而没有任何特殊情况。 在 Java 社区中,关于使任何类单例化的可能方法已经争论了很长时间。 不过,您会发现人们对您提供的任何解决方案都不满意。 它们也不能被否决。 在这篇文章中,我们将讨论一些好的方法,并将尽我们最大的努力。 ```java Table of Contents: 1\. Singleton with eager initialization 2\. Singleton with lazy initialization 3\. Singleton with static block initialization 4\. Singleton with bill pugh solution 5\. Singleton using Enum 6\. Add readResolve() to singleton objects 7\. Add serialVersionUID to singleton objects 8\. Conclusion ``` > 单例术语是从其[数学对应术语](https://en.wikipedia.org/wiki/Singleton_%28mathematics%29 "Singleton in mathematics")中得出的。 如上所述,它希望我们每个上下文只有一个实例。 在 Java 中,每个 JVM 一个实例。 让我们看一下在 Java 中创建单例对象的可能解决方案。 ## 1.立即初始化的单例 这是一种设计模式,其中的类实例实际上是在实际需要之前创建的。 通常,它是在系统启动时完成的。 在热切的初始化单例模式中,无论是否有其他类实际请求其实例,都将创建该单例实例。 ```java public class EagerSingleton { private static volatile EagerSingleton instance = new EagerSingleton(); // private constructor private EagerSingleton() { } public static EagerSingleton getInstance() { return instance; } } ``` 上面的方法很好用,但是有一个缺点。 不论是否在运行时都需要创建实例。 如果此实例不是大对象,并且您可以在不使用它的情况下生存下去,那么这是最好的方法。 让我们用下一种方法解决上述问题。 ## 2.延迟初始化的单例 在计算机编程中,[延迟初始化](https://en.wikipedia.org/wiki/Lazy_initialization "lazy initilization")是将对象的创建,值的计算或其他昂贵的过程推迟到第一次使用时的策略。 在单例模式中,它将限制实例的创建,直到首次请求该实例为止。 让我们在代码中看到这一点: ```java public final class LazySingleton { private static volatile LazySingleton instance = null; // private constructor private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { instance = new LazySingleton(); } } return instance; } } ``` 第一次调用时,上述方法将检查是否已使用`instance`变量创建了实例。 如果没有实例,即实例为`null`,它将创建一个实例并返回其引用。 如果实例已经创建,它将仅返回实例的引用。 但是,这种方法也有其自身的缺点。 让我们看看如何。 假设有两个线程 T1 和 T2。 两者都来创建实例并检查是否`“instance==null”`。 现在,两个线程都将实例变量标识为 null,因此它们都假定必须创建一个实例。 他们依次进入同步块并创建实例。 最后,我们的应用中有两个实例。 可以使用[双检锁](https://en.wikipedia.org/wiki/Double_checked_locking_pattern#Usage_in_Java "double check locking")解决此错误。 该原理告诉我们在同步块中再次重新检查实例变量,如下所示: ```java public class LazySingleton { private static volatile LazySingleton instance = null; // private constructor private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { // Double check if (instance == null) { instance = new LazySingleton(); } } } return instance; } } ``` 上面的代码是单例模式的正确实现。 请确保对实例变量使用[**`volatile`**](https://en.wikipedia.org/wiki/Volatile_variable#In_Java)关键字,否则您可能会遇到乱码的错误情况,在实例实际构造对象之前返回实例的引用,即 JVM 仅分配了内存,而构造器代码仍未执行。 在这种情况下,引用未初始化对象的其他线程可能会引发空指针异常,甚至可能使整个应用崩溃。 ## 3.使用静态块初始化的单例 如果您对类的加载顺序有所了解,则可以使用以下事实:即使在调用构造器之前,也要在类的加载期间执行静态块。 我们可以在单例模式中使用此功能,如下所示: ```java public class StaticBlockSingleton { private static final StaticBlockSingleton INSTANCE; static { try { INSTANCE = new StaticBlockSingleton(); } catch (Exception e) { throw new RuntimeException("Uffff, i was not expecting this!", e); } } public static StaticBlockSingleton getInstance() { return INSTANCE; } private StaticBlockSingleton() { // ... } } ``` 上面的代码有一个缺点。 假设一个类中有 5 个静态字段,并且应用代码只需要访问 2 或 3,那么根本不需要创建实例。 因此,如果我们使用此静态初始化,尽管没有要求,我们将创建一个实例。 下一节将克服此问题。 ## 4\. 单例与 Bill Pugh 解决方案 Bill Pugh 是 [java 内存模型](https://en.wikipedia.org/wiki/Java_Memory_Model "java memory model")更改背后的主要力量。 他的原则“[按需初始化持有人惯例](https://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom "bill pugh principle")”也使用了静态块的想法,但方式有所不同。 建议使用静态内部类。 ```java public class BillPughSingleton { private BillPughSingleton() { } private static class LazyHolder { private static final BillPughSingleton INSTANCE = new BillPughSingleton(); } public static BillPughSingleton getInstance() { return LazyHolder.INSTANCE; } } ``` 如您所见,在需要实例之前,`LazyHolder`类直到需要时才会初始化,您仍然可以使用`BillPughSingleton`类的其他静态成员。 ***这是解决方案,我建议使用。 我在所有项目中都使用了它。*** ## 5.使用枚举的单例 这种类型的实现使用枚举。 [枚举](https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html "enum in java")(如 java 文档中所写)为线程安全提供了隐式支持,并且仅保证了一个实例。 **Java 枚举单例**也是使单例花费最少的好方法。 ```java public enum EnumSingleton { INSTANCE; public void someMethod(String param) { // some class member } } ``` ## 6.将`readResolve()`添加到单例对象 到目前为止,您必须已经决定如何实现单例。 现在,让我们看看即使在面试中也可能出现的其他问题。 假设您的应用是分布式的,并且经常将对象序列化到文件系统中,直到以后需要时才读取它们。 请注意,反序列化总是创建一个新实例。 我们来看一个例子: 我们的单例类是: ```java public class DemoSingleton implements Serializable { private volatile static DemoSingleton instance = null; public static DemoSingleton getInstance() { if (instance == null) { instance = new DemoSingleton(); } return instance; } private int i = 10; public int getI() { return i; } public void setI(int i) { this.i = i; } } ``` 让我们对该类进行序列化,并在进行一些更改后对其进行反序列化: ```java public class SerializationTest { static DemoSingleton instanceOne = DemoSingleton.getInstance(); public static void main(String[] args) { try { // Serialize to a file ObjectOutput out = new ObjectOutputStream(new FileOutputStream( "filename.ser")); out.writeObject(instanceOne); out.close(); instanceOne.setI(20); // Serialize to a file ObjectInput in = new ObjectInputStream(new FileInputStream( "filename.ser")); DemoSingleton instanceTwo = (DemoSingleton) in.readObject(); in.close(); System.out.println(instanceOne.getI()); System.out.println(instanceTwo.getI()); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } Output: 20 10 ``` 不幸的是,两个变量的变量“`i`”的值不同。 显然,该类有两个实例。 因此,我们再次遇到应用中多个实例的相同问题。 要解决此问题,我们需要在`DemoSingleton`类中包含`readResolve()`方法。 当您反序列化对象时,将调用此方法。 在此方法的内部,必须返回现有实例以确保整个实例应用范围。 ```java public class DemoSingleton implements Serializable { private volatile static DemoSingleton instance = null; public static DemoSingleton getInstance() { if (instance == null) { instance = new DemoSingleton(); } return instance; } protected Object readResolve() { return instance; } private int i = 10; public int getI() { return i; } public void setI(int i) { this.i = i; } } ``` 现在,当您执行类`SerializationTest`时,它将为您提供正确的输出。 ```java 20 20 ``` ## 7.将`serialVersionUId`添加到单例对象 到目前为止,一切都很好。 到目前为止,我们已经解决了同步和序列化这两个问题。 现在,我们距正确而完整的实现仅一步之遥。 唯一缺少的部分是序列号。 在您的类结构在序列化和反序列化之间更改的情况下,这是必需的。 更改的类结构将导致 JVM 在反序列化过程中给出异常。 ```java java.io.InvalidClassException: singleton.DemoSingleton; local class incompatible: stream classdesc serialVersionUID = 5026910492258526905, local class serialVersionUID = 3597984220566440782 at java.io.ObjectStreamClass.initNonProxy(Unknown Source) at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source) at java.io.ObjectInputStream.readClassDesc(Unknown Source) at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source) at java.io.ObjectInputStream.readObject0(Unknown Source) at java.io.ObjectInputStream.readObject(Unknown Source) at singleton.SerializationTest.main(SerializationTest.java:24) ``` 仅通过向类添加唯一的串行版本 ID 即可解决此问题。 通过告诉两个类相同,这将防止编译器引发异常,并且仅加载可用的实例变量。 ## 8.结论 在讨论了许多可能的方法和其他可能的错误情况之后,我将向您推荐以下代码模板,以设计您的单例类,该类应确保在上述所有情况下,整个应用中仅一个类的实例。 ```java public class DemoSingleton implements Serializable { private static final long serialVersionUID = 1L; private DemoSingleton() { // private constructor } private static class DemoSingletonHolder { public static final DemoSingleton INSTANCE = new DemoSingleton(); } public static DemoSingleton getInstance() { return DemoSingletonHolder.INSTANCE; } protected Object readResolve() { return getInstance(); } } ``` 我希望这篇文章有足够的信息来帮助您了解**单例模式**和**单例最佳实践**的最常用方法。 让我知道你的想法。 学习愉快! **实时单例示例** – 我只是想添加一些示例,以供进一步研究和在面试中提及: * [`java.awt.Desktop#getDesktop()`](https://docs.oracle.com/javase/6/docs/api/java/awt/Desktop.html#getDesktop%28%29 "Desktop singelton") * [`java.lang.Runtime#getRuntime()`](https://docs.oracle.com/javase/6/docs/api/java/lang/Runtime.html#getRuntime%28%29 "runtime")