【面试常客】单例模式

单例模式是什么

单例模式保证一个类只有一个实例,并且提供一个全局可以访问的入口。取个例子:比如孙悟空的分身术,每一个分身都对应同一个真身
单例模式.png

为什么要用单例模式

  • 节省内存
  • 节省计算,确保计算结果的正确性
  • 方便管理

单例模式使用场景

无状态的工具类

  • 日志工具类
  • 字符串工具类
  • ...

全局信息类

  • 全局技术
  • 环境变量
  • ...

常见单例模式写法

饿汉式

static 修饰实例,private 修饰构造函数

  • 饿汉式
public class Singleton {
    //加载时就完成的实例,避免线程同步的问题
    private static Singleton singleton = new Singleton();
    
    private Singleton() {
    }
    //返回实例
    public static Singleton getInstance(){
        return singleton;
    }
}
  • 饿汉式的变种

写法和饿汉式差不多,只不过是把类实例化的过程放在了静态代码块中。

public class Singleton {
    private static Singleton singleton;

    static {
        singleton = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
        return singleton;
    }
}

优缺点: 在类装载(JVM ClassLoader)的时候就完成了实例化,所以只有这一次,线程是安全的。没有达到懒加载的效果,如果这个实例没有得到使用,就会造成内存的浪费,影响性能,不能保证实例对象的唯一性。

懒汉式

getInstance() 方法被调用的时候才去实例化对象,起到了懒加载的效果,但只能在单线程下使用。单线程进入if判断还没来得及往下执行,另一个线程也通过了这个判断,就会多次创建实例。所以多线程环境下不能使用懒汉式的写法。

  • 线程不安全写法
public class Singleton {
    private static Singleton singleton;

    private Singleton() { }

    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
  • 线程安全写法
public static synchronized  getInstance() {
    ...
}

就在 getInstance() 方法上加上 synchronized 关键字就可以解决之前的线程安全问题。缺点是由于synchronized 关键字会导致退化到串行执行,效率低,大多数情况下是没有必要的。

  • 线程安全进一步优化的写法

采用代码块的形式保护线程安全,synchronized 放到代码块,但是这种写法也是有问题的。加入一个线程进入了第一个if,还没往下执行,而另一个线程也通过了这个判断就会产生多个实例。

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

双重检查式

DCL(Double-Check-Locking) 其实也是在 getInstance() 方法的操作,还给对象加了 volatile 关键字。这里进行了两次if判断保证线程安全,实例化代码只调用一次。后面再访问只会判断第一次的if,跳过整个if快,直接return实例化对象。

public class Singleton {
    private static volatile Singleton singleton;

    private Singleton() { }

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

优缺点 线程安全,延迟加载。因指令重排引起空指针异常。
面试问题为什么要double-check,去电第二次行不行?
需考虑的情况:有两个线程同时调用getInstance() 方法,并且由于singleton是空的,所以两个线程都可以通过第一个if,由于锁机制的存在,会有一个线程先进入同步语句,并进入第二次if判断,而另一个还在等待,当第一个线程执行完 new Singleton() 语句后,就会推出 synchronized保护的区域,这时如果没有第一次if的话,那么第二个线程也会创建一个实例。这就破坏了单例模式。对于第一个check而言,去过去掉了,那么所有线程就会串行执行。

  • 为什么要用voletile关键字?

用voletile关键字的作用主要在于singleton = new Singleton();这并非是一个原子操作,在JVM中,singleton = new Singleton()这语句至少了做了三件事:

  1. 给 singleton 分配内存空间
  2. 调用Singleton的构造函数来初始化singleton
  3. 将singleton对象指向分配的内存空间(此时singleton就不是null了

这里存在重排序的优化,2,3 的顺序是不能保证。有可能是123,也可能是132,如果是后者,那么对象初始化就没有完全初始化,用对象的时候就会报错。
使用voletile的意义就在于防止重排序的发生。

静态内部类式

跟饿汉式的方法才用的机制类似,都采用类装载的方式来保证初始化时只有一个线程。

public class Singleton {
    private static class SingletonInstance {
    private static final Singleton singleton = new Singleton();
    }
    private Singleton() { }

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

优缺点 饿汉式在装载时立刻实例化,静态内部类并不会立刻实例化对象,而是在需要实例化(调用getInstance() )时再实例化。优点跟DCL写法是一样的,都避免了线程不安全的问题,并且延迟加载,性能高。但是他们不能防止被反序列化,生成多个实例。

枚举式

(最佳选择)
借助JDK 1.5 枚举类实现单例模式,不仅能够避免多线程同步,还能防止反序列化和反射创建新的对象。

public enum  Singleton {
    INSTANCE;
    public void whateverMethod(){
        
    }
}

优缺点 :写法简洁、线程安全、避免反序列化
注释 Java专门对枚举类的序列化做了规定:在序列化时,仅仅是将枚举对象的name属性输出到结果中,在反序列化时,通过java.lang.Enum的valueOf方法来根据名字查找对象,而不是创建一个新的对象。反射通过newInstance创建对象时,会检查这个类是否是枚举类。如果是抛出IllgalArgumenException("Cannot reflecively create enum objects")异常,反射创建失败,通过这两种方式来防止反序列化破坏单例模式。

你可能感兴趣的:(设计)