设计模式之单例模式

流过昼夜 提交于 2020-03-12 08:22:18

单例模式

单例模式就是在程序运行中只实例化一次,创建一个全局唯一对象,有点像 Java 的静态变量,但是单例模式要优于静态变量,静态变量在程序启动的时候JVM就会进行加载,如果不使用,会造成大量的资源浪费,单例模式能够实现懒加载,能够在使用实例的时候才去创建实例。开发工具类库中的很多工具类都应用了单例模式,比例线程池、缓存、日志对象等,它们都只需要创建一个对象,如果创建多份实例,可能会带来不可预知的问题,比如资源的浪费、结果处理不一致等问题。

普通懒汉模式

public static LazyMode getInstance1() {
    if (LAZY_MODE == null) {
        LAZY_MODE = new LazyMode();
    }
    return LAZY_MODE;
}

优点

  • 需要实例时才会去构造,不会浪费性能和内存空间

缺点

  • 线程不安全,多线程同时调用方法获取实例时,无法保证实例的唯一性

加锁懒汉模式

public static LazyMode getInstance2() {
    synchronized (LazyMode.class) {
        if (LAZY_MODE == null) {
            LAZY_MODE = new LazyMode();
        }
    }
    return LAZY_MODE;
}

优点

  • 实现懒加载,线程安全

缺点

  • 程序串行化,带来额外的性能损耗

双重判断加锁懒汉模式

public static LazyMode getInstance3() {
    if (LAZY_MODE == null) {
        synchronized (LazyMode.class) {
            if (LAZY_MODE == null) {
                LAZY_MODE = new LazyMode();
            }
        }
    }
    return LAZY_MODE;
}

优点

  • 线程安全,解决了加锁懒汉模式带来的额外性能损失

缺点

  • 多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

    要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile关键字,禁止进行指令重排序即可:

  private static volatile LazyMode LAZY_MODE = null;

饿汉模式

//在饿汉模式中,初始化变量的时候最好加上 final 关键字,这样比较严谨

private static final HungryMode hungryMode = new HungryMode();

private HungryMode() {
}

/**
 * Description : TODO  对外提供唯一可获得实例的方法,因为在类加载的时候就已经完成了初始化,所以不用判断是否为空,同时线程也是安全的<br/>
 */
public static HungryMode getInstance() {
    return hungryMode;
}

优点

  • 由于使用了static关键字,保证了在引用这个变量时,关于这个变量的所以写入操作都完成,所以保证了JVM层面的线程安全

缺点

  • 不能实现懒加载,不管是否是用到了实例,都会生成实例对象,导致了内存空间的浪费并且会降低类的加载速度

内部静态类模式

private static class InnerStaticClassModeInstance{
    //静态类加载时,创建实例
    private static InnerStaticClassMode innerStaticClassModeInstance=new InnerStaticClassMode();
}
/**
 * Description : TODO  构造方法私有化<br/>
*/
private InnerStaticClassMode() {
}
/**
 * Description : TODO  通过内部静态类加载创建唯一实例<br/>
 * 实现简单,懒加载,线程安全
*/
public static InnerStaticClassMode getInstance() {
    return InnerStaticClassModeInstance.innerStaticClassModeInstance;
}

优点

  • 实现简单,懒加载,线程安全,没有性能和内存空间的浪费

    静态内部类单例模式也称单例持有者模式,实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由static修饰,保证只被实例化一次,并且严格保证实例化顺序。

缺点

  • 没得明显缺点,如果不算文件变大的话

枚举类模式

private EnumMode() {
}

/**
 * 枚举类型是线程安全的,并且只会装载一次
 */
private enum Singleton{
    INSTANCE;

    Singleton(){
        instance = new EnumMode();
    }
    private final EnumMode instance;

    private EnumMode getInstance(){

        return instance;
    }
}
/**
 * Description : TODO  <br/>
 * 枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,
 * 枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式
*/
public static EnumMode getInstance(){
    return Singleton.INSTANCE.getInstance();
}

优点

  • 线程安全,不用担心序列化和反射问题(所有单例实现中唯一一种不会被破坏的单例实现模式)

缺点

  • 枚举占用的内存会多一点

破坏单例模式的方法及解决办法

通过反射破坏

  • 反射是通过调用构造方法生成新的对象,所以只要在构造方法中进行判断,若已有实例, 则阻止生成新的实例即可解决。
  //防止通过反射破坏单例模式
  private LazyMode() {
      if (LAZY_MODE != null) {
          throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
      }
  }

通过反序列化破坏

  • 如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,所以只要不实现序列化接口即可解决此问题,如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。
  //防止通过反序列化破坏单例模式
  public Object readResolve() throws ObjectStreamException {
      return LAZY_MODE;
  }
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!