单例模式

197 阅读7分钟

前言

单例模式在平常开发中,用的多,但也错得多。单例模式的核心在于,一个类仅创建一个实例,并提供一个能访问他的方法。

单例模式的要点:

  1. 一个类仅有一个实例
  2. 必须自行创建这个实例
  3. 需要提供一个全局能访问的方法

饿汉模式

public final class Singleton {
    //自行创建静态实例,也意味着在这个类被第一次使用时就会自行创建
    private static Singleton instance = new Singleton();

   //构造函数私有化
    private Singleton() {
        System.out.println("singleton init");
    }

    //提供外部访问方法
    public static Singleton getInstance() {
        return instance;
    }

    public static String get() {
        return "string";
    }
}

这里,我们只是要调用Singleton.get()方法,并不想生成Singleton实例,但没办法已经生成了

public class SingletonTest {
    public static void main(String[] args) {
        System.out.println(Singleton.get());
    }
}

输出

singleton init
string

但在生产过程中,也很难遇到这种情况:一个类,不需要其实例,却把一个或几个常用的静态方法放入这个类中

static修饰成员变量,如何保证单例

在饿汉模式中,static修饰修饰成员变量instance,因此在类的初始化过程中,该变量会被收进类构造器即方法中,在多线程场景下,JVM会保证只有一个线程能够执行该类的方法,其他线程会阻塞等待,等到方法执行完成后,其他线程也不会再执行方法,转而执行自己的代码,也就是说,static修饰成员变量instance,在多线程下能保证只实例一次。

这种方式实现的单例模式,在类初始化阶段,就已经在堆内存中开辟了一块内存,用于存放实例对象,因此为饿汉模式。但在类成员变量比较多的时候,又或者变量比较大的时候,这种模式可能会在没有使用到该对象的情况下,一直占用堆内存。

懒汉模式

懒汉模式就是为了避免直接加载类对象时提前创建对象的一种单例设计模式,只有当使用到类对象时,才会加载实例对象到堆内存中。

public final class Singleton {
    //不实例化
    private static Singleton instance= null;

    //构造函数私有化
    private Singleton(){
    }

    //提供外部访问方法
    public static Singleton getInstance(){
        //当instance为null时,则实例化对象,否则直接返回对象
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

这种方式在单线程运行下是没有问题的,但在多线程下,会出现多个实例对象,当线程1进入if判断条件后,开始实例化对象,此时instance仍为null,线程2进入到if判断后,也会通过判断条件,进行实例化对象。

可以使用Synchronized对方法进行加锁,保证多线程下仅创建一个实例

public final class Singleton {
    //不实例化
    private static Singleton instance= null;
    
   //构造函数私有化
    private Singleton(){
    }

    //加同步锁,保证只有一个线程进入   
    public static synchronized Singleton getInstance(){
        //当instance为null时,则实例化对象,否则直接返回对象
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

但同步锁会增加锁竞争,增加系统开销,导致系统性能下降。在获取类对象时,只有第一次为null,其后每次都不为null,但锁会仍然保证单线程获取

public final class Singleton {
    //不实例化
    private static Singleton instance= null;
    
    //构造函数私有化
    private Singleton(){
    }
   
     public static Singleton getInstance(){
        //当instance为null时,则实例化对象,否则直接返回对象
        if(null == instance){
            //同步锁放进if判断条件,减少同步锁资源竞争
            synchronized (Singleton.class){
                //二次判断
                if(null == instance){
                    instance = new Singleton();
                }
            } 
        }
        return instance;
    }
}

这种方式称为双检锁(Double-Check),能大大提高懒汉模式在多线程下运行性能,但是仍然存在问题,跟Happens-Before重排序有关系。

编译器为了尽可能的减少寄存器的读取、存储次数,会充分复用寄存器的存储值,在下面的代码中,如果不进行重排序的优化,会按照1-2-3执行,如果在编译期间进行重排序优化,就可能会按照1-3-2执行,这样可以减少一次寄存器的存取次数

  1. int a = 1;
  2. int b = 2;
  3. a = a + 1;

在JMM(Java Memory Model)中,重排序优化是很重要的一环,但在重排序优化以提高程序性能,也会给并发编程带来一系列问题。

在执行instance = new Singleton();正常情况下,实例化过程是:

  1. 给Singleton分配内存
  2. 调用Singleton构造函数来初始化成员变量
  3. 将Singleton对象指向分配的内存空间

如果发生重排序优化,步骤3可能会在步骤2之前执行,如果初始化线程刚好完成步骤3,还没进行步骤2时,另外一个线程进行第一次判断,就是非null,就会返回对象使用,但这个时候实际并未完成属性构造,因此在使用时可能会导致异常。这里的Synchronized只能保证可见性、原子性,无法保证执行的顺序。

volatile关键字可以保证线程间变量的可见性禁止指令重排序但不能保证原子性

《深入理解Java虚拟机》:观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令, lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2. 它会强制将对缓存的修改操作立即写入主存;

  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

public final class Singleton {
    //不实例化,volatile修饰
    private volatile static Singleton instance= null;

     //构造函数私有化
    private Singleton(){
    }

    public static Singleton getInstance(){
        //第一次判断,当instance为null时,则实例化对象,否则直接返回对象
        if(null == instance){
          //同步锁
          synchronized (Singleton.class){
             //第二次判断
             if(null == instance){
                instance = new Singleton();
             }
          } 
        }
        return instance;
    }
}

这样volatile修饰的变量的指令操作就不会被重排序,Double-Check懒汉单例模式就万无一失了

内部类实现

在上面的实现,加入了同步锁和Double-Check,复杂且加入了同步锁,再寻求另外一种简单的线程安全的懒加载方式。

可以在Singleton类中创建一个内部类来实现成员变量的初始化,可以避免多线程下重复创建对象的发生,这种方式,只有第一个调用getInstance()方法时,才会加载SingletonInner类,也只有加载SingletonInner类后,才会实例化对象。

public final class Singleton {
  //构造函数私有化
  private Singleton() {
    list = new ArrayList<String>();
  }

  // 内部类实现
  public static class SingletonInner {
    private static Singleton instance=new Singleton();
  }

  public static Singleton getInstance() {
    // 返回内部类中的静态变量
    return SingletonInner.instance;
  }
}

枚举类实现

public class Singleton {
    //构造函数私有化
    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonEnum.SINGLETON_ENUM.getSingleton();
    }

    private enum SingletonEnum {
        SINGLETON_ENUM;

        private Singleton singleton;

        SingletonEnum() {
            singleton = new Singleton();
        }

        public Singleton getSingleton() {
            return singleton;
        }
    }
}

最安全的枚举模式,反射和序列化都是单例

枚举底层实现就是静态内部类,枚举是一种语法糖,在Java编译后,枚举类中的枚举会被声明为static

做个测试

public class Singleton {
    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonEnum.SINGLETON_ENUM.getSingleton();
    }

    public static Singleton getInstance1() {
        return SingletonEnum.SINGLETON_ENUM_1.getSingleton();
    }

    private enum SingletonEnum {
        SINGLETON_ENUM,
        SINGLETON_ENUM_1;

        private Singleton singleton;

        SingletonEnum() {
            System.out.println("enum");
            singleton = new Singleton();
        }

        public Singleton getSingleton() {
            return singleton;
        }
    }
}
public class SingletonTest {
    public static void main(String[] args) {
        System.out.println(Singleton.getInstance());
        System.out.println("=======");
        System.out.println(Singleton.getInstance1());
    }
}

输出

enum
enum
com.***.Singleton@5aaa6d82
=======
com.***.Singleton@73a28541

可以尝试反编译枚举类

enum Day {
	MONDAY, SUNDAY;
}

javac Day.java;

javap Day.class;

反编译后

final class Day extends java.lang.Enum<Day> {
  public static final Day MONDAY;
  public static final Day SUNDAY;
  public static Day[] values();
  public static Day valueOf(java.lang.String)
  static {};
}

从反编译的代码可以看出,编译器生成了一个Day类(注意该类是final类型的,将无法别继承),而且继承自java.lang.Enum类,编译器还生成了两个Day类型的实例对应枚举中的定义,因此枚举类实现单例模式,也是通过JVM去保证。