【设计模式】单例创建的五种方式

274 阅读6分钟

写在前面

单例模式属于23种设计模式中的创建型模式范畴,聚焦于对象的创建;由于整个进程中只有一个实例,所以不但减少了内存的占用,不存在创建多对象带来资源消耗,而且保证了数据的一致性

单例模式关键在于如何保证只实例化出一个对象,本文基于此目的,将具体介绍五种创建单例的方案

单例模式实现特点

(1)声明一个静态私有的成员变量

用于持有类本身实例化出来的对象。

(2)私有化构造函数

目的是防止外部直接通过new方式误创建,但是享有特权的客户端可通过反射绕过私有的构造函数,创建出多个对象,所以很多时候也是契约式编程,并不能说反射特性的存在就认为下面提到的几种创建机制是不安全的。

(3)声明一个静态公共的函数,函数返回该对象的实例

用于提供客户端调用。

下面每种创建方案可以对照着上述的实现特点,看看哪些符合,哪些不需要。

饿汉模式

public class Singleton1 {
   private static final Singleton1 INSTANCE = new Singleton1();

   private Singleton1() {
   }

   public static Singleton1 getInstance() {
      return INSTANCE;
   }

   public void showName() {
      System.out.println(this.getClass().getSimpleName());
   }
}

该方式直接在静态成员变量中直接通过new方式创建了对象,非常简洁明了;该方式的实例是在类被加载的时候被创建的,所以天然是线程安全的。

网上看到不少博客说该实现方式可能在对象没有真正被使用时就被创建,比如该类存在另外的静态方法,调用该静态方法时促使类被加载,然后实例随之被创建。我个人觉得如果一个类打算定义成单例,那么这个类就不应该再存在除了获取对象方法之外的其他静态方法,因为静态方法和单例的实例方法一定程度上是等价的,同时使用有些冗余和不合理;如果非要定义静态方法,比如一个工具类,那么就干脆把所有方法都定义成静态方法,取消单例实现。另外Android Studio IDE中创建一个类,如果选择了单例,IDE也是默认用该方案来自动实现的,所以我认为可以放心大胆的使用。

懒汉模式——锁方法

public class Singleton2 {
   private static Singleton2 INSTANCE;

   private Singleton2() {
   }

   public static synchronized Singleton2 getInstance() {
      if (INSTANCE == null) {
         INSTANCE = new Singleton2();
      }
      return INSTANCE;
   }

   public void showName() {
      System.out.println(this.getClass().getSimpleName());
   }
}

该方式的实例是在getInstance方法调用时被创建了,为了解决线程安全,直接在方法上加synchronized关键字,由于INSTANCE只会在第一次被调用时为空,这时锁是有意义的,之后INSTANCE都不会为空,而锁依然存在,这时锁只会影响性能,并无其他好处。

懒汉模式——锁代码块+双重校验

public class Singleton3 {
   // 注意点2
   private static volatile Singleton3 INSTANCE;

   private Singleton3() {
   }

   public static Singleton3 getInstance() {
      if (INSTANCE == null) {
         synchronized (Singleton3.class) {
            // 注意点1
            if (INSTANCE == null) {
               INSTANCE = new Singleton3();
            }
         }
      }

      return INSTANCE;
   }

   public void showName() {
      System.out.println(this.getClass().getSimpleName());
   }
}

该方案是为了解决上一种方案中锁影响性能而进行改进的方案,首先当INSTANCE不为空时,不会进入加锁的代码块,所以此场景下不会影响任何性能。

另外该方案有两处需要注意,在代码中注释的注意点1进行了二次判空校验,原因会存在两个线程同时完成了第一次的判空判断,然后都进入了INSTANCE为空的分支,即加锁的代码块,其中先获取锁的线程完成了实例的创建,然后释放锁,这时另一个线程执行加锁的代码块,如果没再次判空,对象将会被再一次创建。

注意点2是静态成员变量多一个关键字volatilevolatile主要具有解决CPU高速缓存的可见性问题防止指令重排的两个作用,此处是使用到它的防止指令重排的作用,先看看该方案中的下面这行代码

INSTANCE = new Singleton3();

该行代码其实完成了三件事:(1)给实例申请内存空间,(2)实例初始化,(3)变量指向内存空间;JVM由于指令优化的存在,三件事并不一定按照1/2/3的顺序执行的,可能先执行了(3),再执行(2),而一旦(3)被执行,INSTANCE将不会是null,但是这时的实例由于没有完成初始化,是一个不完整的实例,这个不完整的实例可能被另一个线程直接获取使用,从而出错。而加了volatile之后,JVM将不会进行指令重排,即按照1/2/3顺序执行,这时其他线程获取到不为空的INSTANCE,肯定是一个完整可用的实例。

内部静态类模式

public class Singleton4 {
   private Singleton4() {
   }

   public static Singleton4 getInstance() {
      return SingletonHolder.INSTANCE;
   }

   public void showName() {
      System.out.println(this.getClass().getSimpleName());
   }

   private static class SingletonHolder {
      private static final Singleton4 INSTANCE = new Singleton4();
   }
}

先分析下该种方式实例被创建的时机,首先需要知道内部静态类功能上是等价于外部类的,内部静态类SingletonHolder只会在调用Singleton4getInstance才被加载,这时其静态成员变量被初始化,即创建出来了单实例Singleton4

由于实例的创建是在类加载时创建的,所以天然具有线程安全的特点,即不需要懒汉模式的加锁。另外由于SingletonHolder只会在调用Singleton4getInstance才被加载,换句话说,即使Singleton4类中包含有其他的静态方法,当调用这些静态方法时,触发了Singleton4类被加载,也不会导致实例被创建,从这个角度上看又更优于第一种饿汉模式,即不那么依赖开发者的编程规范。

枚举模式

public enum Singleton5 {
   INSTANCE;

   public void showName() {
      System.out.println(this.getClass().getSimpleName());
   }
}

通过枚举方式创建同样也是线程安全的,同时还携带着枚举的优点:无偿的提供了序列化机制,绝对防止多次实例化,即使面对复杂的序列化和反射调用时。

总结

本文主要介绍了单例模式的实现特点,以及五种创建方式的实现细节,和各自有哪些特点,是通过何种方式来保证安全的被创建;单例的创建不可避免的涉及到了类的加载,多线程安全,以及volatile的功能等,学习相关知识会更加有助于对单例的理解。

最后想说的是,在实际使用中一定要结合业务场景来判断是否需要使用单例,只有正确适当的使用它,才能发挥出单例的优势来,否则可能会造成内存泄露(如:单例持有着短生命周期的对象导致其无法被回收)等一系列其他问题。

欢迎大家的交流讨论,互相成长~