面试高频问题之单例模式——从三个方面分析Java中常用的7种单例模式设计的优与劣

2020-10-01   69 次阅读


单例模式是GoF23种最常用的设计模式之一,也是经常被面试问到的问题。单例模式提供了一种在多线程情况下保证实例唯一性的解决方案。单例模式实现虽然简单,但是实现方式却多种多样,本文从线程安全、高性能、懒加载三个维度逐一分析。

1、饿汉模式

public class Singleton{   
	private static Singleton instance = new Singleton();   
		private Singleton () { 
	}   
	public static Singleton getInstance() {       
	return instance;   
	}
}

饿汉模式最为简单粗暴,将instance作为静态类变量案,使得instance在类初始化时得到实例化,百分百保证同步。但由于实例化的对象,可能很长一段时间才会被使用到,也就是使得该对象长久的占据堆内存而未被使用,所以除非单例对象的类成员不多,且在程序运行早起就被使用,否则不推荐该方法。

2、懒汉模式

public class Singleton{
      private static Singleton instance;
      private Singleton(){}
       public static Singleton getInstance(){
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
      }} 

相比于饿汉模式,懒汉模式的区别在于instance并未一开始就实例化,而是在被使用的时候才实例化,这样就避免了类在初始化时就提前创建了对象。但这样使得在多线程的情况下,不能保证对象只被创建一次,也就不能保证单例的唯一性。

3、懒汉+同步方法

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

懒汉模式可以保证实例的懒加载,但无法保证实例的唯一性,为了解决这个问题引申出了懒汉同步的方式,即加上 synchronized 关键字,在创建对象的时候,做同步约束。但是,懒汉+同步的方式,虽然解决做到了懒加载,同时也做了同步约束,但由于synchronized关键字的排他性,使得getInstance()方法性能下降了。

4、双重校验锁

public class Singleton {
    private static Singleton singleton;
    private Connection conn;
    private Socket socket;
    private Singleton () {
	this.conn = //初始化
	this.socket = //初始化
    }
    public static Singleton getSingleton() {
       if (singleton == null) {
           synchronized (Singleton.class) {
              if (singleton == null) {
                  singleton = new Singleton();
              }
           }
       }
       return singleton;
    }
 }

鉴于懒汉+同步的方式,依然不能满足我们的要求,再次基础上继续优化,出现了双重校验锁的方式。依然需要判断singleton是否为空,只是区别在于singleton作用的对象不再是getSingleton()方法,而是作用于整个单例类,两次校验singleton,但并不阻塞getSingleton()方法。

双重校验锁的方式看似完美,但有一个致命的缺陷:在Singleton的构造函数中,需要分别初始化conn和socket,但是根绝JVM指定重排序和Happens-Before规则,singleton、conn、socket三者之间的实例化顺序无前后约束的关系,就可能是singleton初始化完成,而conn、socket未被初始化完成,而如果这个时候singleton被调用,使用了conn、socket其中之一,则会导致 空指针异常

5、volatile+双重校验锁

public class Singleton {
    private volatile static Singleton singleton;
    private Singleton () {
	
    }
    public static Singleton getSingleton() {
       if (singleton == null) {
           synchronized (Singleton.class) {
              if (singleton == null) {
                  singleton = new Singleton();
              }
           }
       }
       return singleton;
    }
 }

可以看到,在双重校验锁的基础上,加上volatile关键字,防止JVM在运行时的指定重排序,就避免了双重校验锁可能导致的空指针异常*

6、Holder——静态内部类

public class Singleton {
    private static class SingletonHolder{
       private static final Singleton INSTSNCE = new Singleton();
    }
    private Singleton () {}
    public static final Singleton getInstance () {
        return SingletonHolder.INSTANCE;
    }
 }

Holder方式完全借助了类加载的特点,在Singleton类中并没有上面列举的方法那样存在静态成员,而是将其放到了静态内部类SingletonHolder中,因此在Singleton类的初始化过程中并不会创建Singleton的实例。SingletonHolder类中定义了Singleton的静态变量并直接进行了实例化,当SingletonHolder被引用的时候才会创建Singleton的实例。根据类加载机制的特点,这样能保证Singleton不会提前被创建,同时又保证安全。

7、枚举

public class Singleton {
    INSTANCE;
    Singleton () {}
    public static void whateverMethod() {
    
    }
    public static Singleton getInstance () {
        return INSTANCE;
    }
 }

根据枚举的特性,枚举类型不允许被继承,同样是线程安全且只能被实例化一次,但是枚举不能够做到懒加载。

即便如此,我们可以利用Holder的方式对枚举模式进行改造,将静态内部类改为内部枚举,改法可以自行摸索,不再举例说明。

Holder和枚举方式的单例设计是最好的设计,这种两种方式都是Effective Java作者Josh Bloch 提倡的方式,它们不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

毕生所求无它,爱与自由而已