我们接下去几篇设计模式会介绍几种创造型模式(单例,工厂,抽象工厂,建造者)。
创建型模式(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