单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,本文尝试分析每种实现下获取单例对象(即getInstance)时的线程安全(创建或获取时),并思考注释中的问题。

饿汉式:类加载就会导致该单实例对象被创建 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会被创建

1. 饿汉单例

  1. // 问题1: 为什么加final
  2. // 问题2: 如果实现了序列化接口,还要做什么来防止反序列化破坏单例
  3. public final class Singleton implements Serializable {
  4. // 问题3: 为什么设置私有?是否能防止反射创建新的实例?
  5. private Singleton(){}
  6. // 问题4: 这样初始化是否能保证单例对象创建时的线程安全?
  7. private static final Singleton INSTANCE = new Singleton();
  8. // 问题5: 为什么提供静态方法而不是直接将INSTANCE设置为public, 说出你的理由
  9. public static Hunger getInstance(){
  10. return INSTANCE;
  11. }
  12. public Object readResolve(){
  13. return INSTANCE;
  14. }
  15. }

答:

  1. 加final是防止子类不适当的方法覆盖父类破坏单例
  2. 实现readResolve方法,如上文所示,返回该单例
  3. 设置私有防止其他类调用构造创建对象,不能防止反射,因为反射可以暴力破坏私有访问条件
  4. 能,静态成员变量初始化是在类加载阶段完成,由jvm保证线程安全
  5. 方法可以提供更好的封装,方便内部改进实现懒惰加载,可以对单例对象有更多控制,可以提供泛型支持等

2. 枚举单例

  1. // 问题1:枚举单例是如何限制实例个数的
  2. // 问题2:枚举单例在创建时是否有并发问题
  3. // 问题3:枚举单例能否被反射破坏单例
  4. // 问题4:枚举单例能否被反序列化破坏单例
  5. // 问题5:枚举单例属于懒汉式还是饿汉式
  6. // 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
  7. enum Singleton {
  8. INSTANCE;
  9. }

答:

  1. 枚举定义的枚举对象定义时有几个就有几个,相当于枚举类的静态成员变量(public static final)
  2. 没有,其成员变量也是在类加载时创建
  3. 不能
  4. 不能,默认实现序列化接口,实现时考虑到了这个问题
  5. 饿汉式
  6. 加入构造方法即可

    3. 懒汉单例

    1. public final class Singleton {
    2. private Singleton() { }
    3. private static Singleton INSTANCE = null;
    4. // 分析这里的线程安全, 并说明有什么缺点
    5. public static synchronized Singleton getInstance() {
    6. if( INSTANCE != null ){
    7. return INSTANCE;
    8. }
    9. INSTANCE = new Singleton();
    10. return INSTANCE;
    11. }
    12. }

    答:
    可以保证线程安全,static上的synchronized方法将锁加在了类对象上。缺点是锁的范围比较大,性能比较低

    4. DCL 懒汉单例(双重校验锁)

    1. public final class Singleton {
    2. private Singleton() { }
    3. // 问题1:解释为什么要加 volatile ?
    4. private static volatile Singleton INSTANCE = null;
    5. // 问题2:对比实现3, 说出这样做的意义
    6. public static Singleton getInstance() {
    7. // 第一层校验加快速度,提高性能
    8. if (INSTANCE != null) {
    9. return INSTANCE;
    10. }
    11. synchronized (Singleton.class) {
    12. // 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
    13. // 第二层校验避免并发创建多个对象
    14. if (INSTANCE != null) { // t2
    15. return INSTANCE;
    16. }
    17. INSTANCE = new Singleton();
    18. return INSTANCE;
    19. }
    20. }
    21. }

    答:

  7. 因为synchronized方法里面构造方法的指令可能被重排,有可能导致一个线程在创建单例时,另一个线程获取了尚未初始化的单例对象。要使用volatile防止指令重排

  8. synchronized范围缩小了,性能更强,只在第一次创建对象时需要加锁
  9. 防止首次创建对象时并发的问题

也可以有一下通用形式:

  1. public class Singleton {
  2. private volatile static Singleton uniqueInstance;
  3. private Singleton() {
  4. }
  5. public static Singleton getUniqueInstance() {
  6. if (uniqueInstance == null) {
  7. synchronized (Singleton.class) {
  8. if (uniqueInstance == null) {
  9. uniqueInstance = new Singleton();
  10. }
  11. }
  12. }
  13. return uniqueInstance;
  14. }
  15. }

5. 静态内部类懒汉单例

  1. public final class Singleton {
  2. private Singleton() { }
  3. // 问题1:属于懒汉式还是饿汉式
  4. private static class LazyHolder {
  5. static final Singleton INSTANCE = new Singleton();
  6. }
  7. // 问题2:在创建时是否有并发问题
  8. public static Singleton getInstance() {
  9. return LazyHolder.INSTANCE;
  10. }
  11. }

答:

  1. 懒汉式,类加载本身就是懒惰的,静态内部类加载时只有在用到时才会发生
  2. 没有,类加载有jvm保证线程安全

推荐使用枚举和静态内部类懒汉单例