我们接下去几篇设计模式会介绍几种创造型模式(单例,工厂,抽象工厂,建造者)。
创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。

创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。

定义

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

优缺点

优点

  • 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
  • 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。
  • 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

缺点

  • 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。
  • 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。
  • 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。

应用

Spring中每一个Bean都是单例的,Spring容器可以管理这些Bean的生命期,决定什么时候创建出来,什么时候销毁,销毁的时候要如何处理,等等。

实现

饿汉式(线程安全,可用)

在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。 这时候初始化instance没有达到懒加载的效果,也就是不管怎样内存中都会存在这样的对象。

public class Singleton {

   /**
    * 类加载时就初始化
    */
   private static final Singleton INSTANCE = new Singleton();

   private Singleton() {
   }

   public static Singleton getInstance() {
      return INSTANCE;
   }
}

懒汉模式(线程不安全,不可用)

懒汉模式申明了一个静态对象,在用户第一次调用时初始化,虽然节约了资源,在多线程不能正常工作。

public class SingletonNotSafe {
   private static SingletonNotSafe instance;

   private SingletonNotSafe() {
   }

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

}

懒汉式(线程安全,效率低不推荐)

为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。

public class SingletonSafe {
   private static SingletonSafe instance;

   private SingletonSafe() {
   }

   public static synchronized SingletonSafe getInstance() {

      if (instance == null) {
         instance = new SingletonSafe();
      }
      return instance;
   }
}

虽然做到了线程安全,并且解决了多线程的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时执行,即第一次创建单例实例对象时,其他时候直接return实例即可。

双重校验锁(线程安全,可用)

第一次校验:由于单例模式只需要创建一次实例,如果后面再次调用getInstance()方法时,则直接返回之前创建的实例,因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。如果不加第一次校验的话,那跟上面的懒汉模式没什么区别,每次都要去竞争锁。

第二次校验:如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。

需要注意的是,private volatile static Singleton instance; 需要加volatile关键字,否则会出现错误。问题的原因在于JVM指令重排优化的存在。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance(),取到的就是状态不正确的对象,程序就会出错。

public class Singleton {

   private volatile static Singleton instance;

   private Singleton() {
   }

   public static Singleton getInstance() {
      //第一次校验
      if (instance == null) {
         synchronized (Singleton.class) {
            //第二次校验
            if (instance == null) {
               instance = new Singleton();
            }
         }
      }
      return instance;
   }

}

静态内部类(线程安全,可用)

使用JVM本身机制保证了线程安全问题。由于 SingletonHolder 是私有的,在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonHolder 类,从而完成 Singleton 的实例化。

类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

public class Singleton {

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

   private Singleton() {
   }

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

枚举(线程安全,可用)

枚举的书写非常简单,访问也很简单在这里 SingleTon.INSTANCE 即为 SingleTon 类型的引用所以得到它就可以调用枚举中的方法了。创建枚举默认就是线程安全的。

public enum SingleTon {

   INSTANCE;

   private SingleTon() {
   }

   public void method() {
   }

}

参考

《设计模式之禅》
https://design-patterns.readthedocs.io/zh_CN/latest/index.html