Java Singleton设计模式最佳实践与示例

时间:2020-02-23 14:36:51  来源:igfitidea点击:

Java Singleton模式是"四个设计"模式的帮派之一,位于"创新设计模式"类别中。

从定义上看,这似乎是一个非常简单的设计模式,但是当涉及到实现时,它会带来很多实现方面的问题。

Java Singleton模式的实现一直是开发人员中有争议的话题。
其中我们将学习Singleton设计模式的原理,实现Singleton设计模式的不同方法以及使用它的一些最佳实践。

单例模式

  • 单例模式限制了一个类的实例化,并确保在Java虚拟机中仅存在该类的一个实例。

  • 单例类必须提供全局访问点才能获取该类的实例。

  • 单例模式用于日志记录,驱动程序对象,缓存和线程池。

  • Singleton设计模式还用于其他设计模式,例如Abstract Factory,Builder,Prototype,Facade等。

  • Singleton设计模式也用于核心Java类,例如," java.lang.Runtime"," java.awt.Desktop"。

Java单例模式实现

要实现Singleton模式,我们有不同的方法,但是所有方法都具有以下共同概念。

  • 私有构造函数,用于限制该类从其他类的实例化。

  • 同一类的私有静态变量,是该类的唯一实例。

  • 返回类实例的公共静态方法,这是外部世界获取单例类实例的全局访问点。

在进一步的章节中,我们将学习Singleton模式实现的不同方法以及与实现有关的设计问题。

在急切的初始化中,在加载类时会创建Singleton类的实例,这是创建Singleton类的最简单方法,但是它存在一个缺点,即使客户端应用程序可能不使用它也会创建该实例。

1.渴望初始化

这是静态初始化单例类的实现。

如果您的单例类没有使用大量资源,则可以使用这种方法。
但是在大多数情况下,都是为文件系统,数据库连接等资源创建Singleton类的。
除非客户端调用getInstance方法,否则应避免实例化。
另外,此方法不提供任何用于异常处理的选项。

package com.theitroad.singleton;

public class EagerInitializedSingleton {
  
  private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
  
  //private constructor to avoid client applications to use constructor
  private EagerInitializedSingleton(){}

  public static EagerInitializedSingleton getInstance(){
      return instance;
  }
}

静态块初始化的实现与渴望的初始化类似,不同的是,类的实例是在提供了异常处理选项的静态块中创建的。

2.静态块初始化

急切的初始化和静态块初始化都在实例被使用之前就创建了实例,这不是最佳实践。
因此,在进一步的章节中,我们将学习如何创建支持延迟初始化的Singleton类。

package com.theitroad.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 occured in creating singleton instance");
      }
  }
  
  public static StaticBlockSingleton getInstance(){
      return instance;
  }
}

实现单例模式的惰性初始化方法在全局访问方法中创建实例。
这是使用这种方法创建Singleton类的示例代码。

3.延迟初始化

上面的实现在单线程环境下可以很好地工作,但是对于多线程系统,如果多个线程同时位于if条件中,则可能导致问题。
它将破坏单例模式,并且两个线程都将获得单例类的不同实例。
在下一节中,我们将介绍创建线程安全的单例类的不同方法。

package com.theitroad.singleton;

public class LazyInitializedSingleton {

  private static LazyInitializedSingleton instance;
  
  private LazyInitializedSingleton(){}
  
  public static LazyInitializedSingleton getInstance(){
      if(instance == null){
          instance = new LazyInitializedSingleton();
      }
      return instance;
  }
}

创建线程安全的单例类的更简单方法是使全局访问方法同步,以便一次只能有一个线程执行此方法。
这种方法的一般实现类似于下面的类。

4.线程安全单例

上面的实现可以很好地工作并提供线程安全性,但是由于与同步方法相关的成本,它降低了性能,尽管我们仅对可能创建单独实例的前几个线程需要它(请参阅:Java同步)。
为了避免每次另外的开销,使用了双重检查的锁定原理。
在这种方法中,在if条件中使用了同步块,并进行了附加检查,以确保仅创建一个singleton类的实例。

package com.theitroad.singleton;

public class ThreadSafeSingleton {

  private static ThreadSafeSingleton instance;
  
  private ThreadSafeSingleton(){}
  
  public static synchronized ThreadSafeSingleton getInstance(){
      if(instance == null){
          instance = new ThreadSafeSingleton();
      }
      return instance;
  }
  
}

以下代码片段提供了双重检查的锁定实现。

阅读:线程安全单例类

public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
  if(instance == null){
      synchronized (ThreadSafeSingleton.class) {
          if(instance == null){
              instance = new ThreadSafeSingleton();
          }
      }
  }
  return instance;
}

在Java 5之前,Java内存模型存在很多问题,并且上述方法在某些情况下会失败,在某些情况下,太多的线程试图同时获取Singleton类的实例。
因此,比尔·普格(Bill Pugh)提出了另一种方法,以使用内部静态帮助程序类创建Singleton类。
Bill Pugh Singleton的实现是这样的;

5. Bill Pugh Singleton执行

请注意,内部私有静态类包含单例类的实例。
当加载singleton类时,SingletonHelper类不会加载到内存中,只有当有人调用getInstance方法时,该类才会加载并创建Singleton类实例。

package com.theitroad.singleton;

public class BillPughSingleton {

  private BillPughSingleton(){}
  
  private static class SingletonHelper{
      private static final BillPughSingleton INSTANCE = new BillPughSingleton();
  }
  
  public static BillPughSingleton getInstance(){
      return SingletonHelper.INSTANCE;
  }
}

这是Singleton类使用最广泛的方法,因为它不需要同步。
我在许多项目中都使用了这种方法,而且也很容易理解和实施。

阅读:Java嵌套类

反射可用于销毁所有上述单例实现方法。
让我们用一个示例类来看看。

6.使用反射破坏单例模式

当您运行上述测试类时,您会注意到两个实例的hashCode不同,这会破坏单例模式。
反射功能非常强大,并在诸如Spring和Hibernate的许多框架中使用,请查看Java Reflection Tutorial。

package com.theitroad.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) {
              //Below 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());
  }

}

为了通过反射来克服这种情况,Joshua Bloch建议使用Enum来实现Singleton设计模式,因为Java确保在Java程序中仅将一次枚举值实例化一次。
由于Java枚举值可全局访问,因此单例也是如此。
缺点是枚举类型有些不灵活;例如,它不允许延迟初始化。

7.枚举辛格顿

package com.theitroad.singleton;

public enum EnumSingleton {

  INSTANCE;
  
  public static void doSomething(){
      //do something
  }
}

有时在分布式系统中,我们需要在Singleton类中实现Serializable接口,以便我们可以将其状态存储在文件系统中,并在以后的某个时间点检索它。
这是一个小的单例类,它也实现了Serializable接口。

8.序列化和单例

序列化单例类的问题在于,每当我们反序列化它时,它将创建该类的新实例。
让我们用一个简单的程序看看它。

package com.theitroad.singleton;

import java.io.Serializable;

public class SerializedSingleton implements Serializable{

  private static final long serialVersionUID = -7604766932016737115L;

  private SerializedSingleton(){}
  
  private static class SingletonHelper{
      private static final SerializedSingleton instance = new SerializedSingleton();
  }
  
  public static SerializedSingleton getInstance(){
      return SingletonHelper.instance;
  }
  
}

上面程序的输出是:

package com.theitroad.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();
      
      //deserailize 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());
      
  }

}

因此,它破坏了单例模式,以克服这种情况,我们需要做的所有事情就是提供readResolve()方法的实现。

instanceOne hashCode=2011117821
instanceTwo hashCode=109647522

此后,您将注意到两个实例的hashCode在测试程序中相同。

protected Object readResolve() {
  return getInstance();
}