单例模式

118 阅读4分钟

单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,一个类只有一个实例,即一个类只会存在一个对象实例。单例模式的实现需要注意以下两点:

  1. 类能够有返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称)。
  2. 将该类的构造方法定义为私有方法,这样就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

在介绍单例模式的实现方式之前,需要强调一点:在多线程场景下,单例模式可能会出现线程安全问题,带来的后果就是会创建出多个对象实例,从而违反了单例模式中实例唯一的原则,针对这个问题通常使用锁来解决。

饿汉式(静态变量)

public class Singleton {

    private static Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}
  • 优点:静态变量在类加载阶段就被创建好了,根据jvm的类加载机制可知,类加载阶段线程安全,避免了线程安全问题

  • 缺点:类加载阶段就完成了实例创建,如果该对象实例一直未被使用,也是一种内存浪费

懒汉式(线程不安全)

public class Singleton {

    private static Singleton INSTANCE;

    private Singleton() {}

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}
  • 优点:是一种懒加载机制,开始时并未创建出对象实例,而是在使用时才创建出对象实例
  • 缺点:在多线程场景下,一旦多个线程执行到if (INSTANCE == null),并进行了线程上下文切换,此时就可能会创建出多个对象实例

懒汉式(通过synchronized保证线程安全)

public class Singleton {

    private static Singleton INSTANCE;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}
  • 优点:获取实例的方法进行加锁,确保了INSTANCE只会被创建一份
  • 缺点:针对获取实例的方法加锁,锁粒度太大,因为一旦INSTANCE!=null,其实也就没这些问题,考虑将锁拿到方法内部,进行以下改造:
public class Singleton {

    private static Singleton INSTANCE;

    private Singleton() {}

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class){
                INSTANCE = new Singleton();
            }
        }
        return INSTANCE;
    }
}

这种方式其实还是没有解决线程安全的问题,无非就是提高了方法的执行效率。当下的问题就是解决前面所提到的【一旦多个线程执行到if (INSTANCE == null),并进行了线程上下文切换,此时就可能会创建出多个对象实例】。

简而言之,目前的问题是:当一个线程进行上下文切换时,只感知到了上下文切换前INSTANCE的状态,而上下文切换后并没有去校验INSTANCE的状态。由此引出DCL双锁检测

懒汉式(DCL双锁检测)

public class Singleton {
  private volatile static Singleton singleton;	
  public static Singleton getInstance() {
      //检查实例,如果不存在,就进入同步代码块
      if (singleton == null) {
        //竞争锁 原子操作
          synchronized (Singleton.class) {
              //进入同步代码块后,再检查一次,如果仍是null,才创建实例
              if (singleton == null) {
                  singleton = new Singleton();
              }
          }
      }
      return singleton;
  }
}

DCL双锁检测这种实现方式,有以下几点变化:

  1. 同步代码块内再次进行了一次判空校验(也就是线上上下文切换前后都校验了
  2. 变量singleton使用了volatile进行修饰(下篇文章好好聊下为啥使用了volatile,并不像八股中说的那么浅)

综合可见,这种实现方式的优点:线程安全、延迟加载、效率较高。

懒汉式(静态内部类)

public class Singleton {
    private Singleton() {}

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

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

静态内部类的优点是:外部类加载时并不会立即加载内部类,内部类不被加载则不去创建INSTANCE。即当Singleton第一次被加载时,并不会去加载SingletonInstance,只有当getInstance()方法第一次被调用时,需要去访问内部类中的属性时,才会触发内部类的加载动作,去创建INSTANCE,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化

除了上面总结的实现方式外,还有枚举这种天然单例的实现方式,但是我没咋用过,所以就不班门弄斧了。