设计模式之单例模式

75 阅读4分钟

1. 单例模式介绍

案例:皇帝,中国历朝历代,很少出现两个皇帝并存的时候,我们把皇帝看作是一个单例,在一位皇帝即位期间,大臣朝拜的皇帝应该只能是一个。

类图:

代码:

/**
 * @Description 皇帝
 */
public class Emperor {

  // 定义一个皇帝
  private static Emperor emperor = null;
  
  private Emperor() {
    // 私有构造器,目的就是不能让客户端通过new的方式创建一个皇帝对象
  }
  
  // 获取皇帝对象只能通过对外暴露的这个方法
  public static Emperor getInstance() {
    // 如果皇帝还没有定义,那就定一个
    if(emperor == null) {
      emperor = new Emperor();
    }
    return emperor;
  }
  
  // 打印皇帝名字
  public static void emperorInfo() {
    System.out.println("康熙大帝...");
  }
  
}

/**
 * @Description 大臣
 */
public class Minister {

  public static void main(String[] args) {
    // 第一天
    Emperor emperor1 = Emperor.getInstance();
    emperor1.emperorInfo();
    
    // 第二天
    Emperor emperor2 = Emperor.getInstance();
    emperor2.emperorInfo();
    
    // 第二天
    Emperor emperor3 = Emperor.getInstance();
    emperor3.emperorInfo();
    
    // 无论什么时候朝拜皇帝,都是同一个人
    
  }
  
}

以上代码是一个最简单的单例模式的实现,即将构造器私有化,提供唯一的获取对象实例的方法,保证每个访问的都是同一个对象。这种实现方式简单,但未考虑多线程环境下多次创建对象等其他问题。

2. 5种单例模式

2.1 饿汉式单例模式

public class HungerSingleton {

    private static HungerSingleton instance = new HungerSingleton();

    private HungerSingleton() {}

    public static HungerSingleton getInstance() {
        return instance;
    }

}
  • 特点:static 变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题,虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题,因此可以省略 synchronized 关键字
  • 问题:如果只是加载了本类,而没有调用 getInstance() 方法,那么这个单例对象存在就没有意义,造成资源浪费

2.2 懒汉式单例模式

public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton() {}

    public static synchronized LazySingleton getInstance() {
        if(instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }

}
  • 特点:lazy load,延迟加载,懒加载,真正用的时候才加载,提高了资源利用率
  • 问题:每次调用getInstance()方法都要同步,并发效率较低

2.3 双重检测锁式单例模式

public class DoubleCheckSingleton {

    private static DoubleCheckSingleton instance;

    private DoubleCheckSingleton() {}

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

}
  • 特点:将同步内容写到 if() 内部,提高了并发执行的效率,不必每次获取对象都进行同步,只有第一次才同步,创建以后就没有必要了
  • 问题:由于编译器优化和JVM底层内部模型原因,偶尔会出现问题。不建议使用

2.4 静态内部类式单例模式

public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {}

    private static class StaticInnerClassSingletonInstance {
        private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return StaticInnerClassSingletonInstance.instance;
    }

}
  • 特点:外部类没有 static 属性,不会像饿汉式那样立即加载,只有真正调用 getInstance() 方法,才会加载静态内部类,加载类时是线程安全的,instance 是 static final 类型,保证了内存中只有这样一个实例存在而且只能被赋值一次,从而保证了线程安全性,兼顾了并发高效调用和延迟加载的优势

2.5 枚举单例

public enum EnumSingleton {

    // 定义一个枚举的元素,它就代表了 EnumSingleton 的一个实例
    INSTANCE;

    public static void main(String[] args) {
        // 需要的时候直接调用就可以
        EnumSingleton instance = EnumSingleton.INSTANCE;
        // 其他操作...
    }

}
  • 特点:实现简单,枚举本身就是单例模式,由 JVM 从根本上提供保障,避免反射和序列化的漏洞
  • 问题:不能延迟加载

2.6 5种单例模式的比较

线程安全延迟加载性能能否抵御反射和序列化的破解其他
饿汉式
懒汉式
双重检测锁由于JVM底层内部模型原因,偶尔会出问题
静态内部类
枚举

如何选择单例模式:

  • 单例对象占用资源少,不需要延时加载:枚举式好于饿汉式
  • 单例对象占用资源多,需要延时加载:静态内部类式好于懒汉式和双重检测锁式

3. 破解单例模式

3.1 使用反射破解单例模式

import java.lang.reflect.Constructor;

public class CrackSingletonByReflectDemo {

    private static CrackSingletonByReflectDemo instance = new CrackSingletonByReflectDemo();

    private CrackSingletonByReflectDemo() {
        
    }

    public static CrackSingletonByReflectDemo getInstance() {
        return instance;
    }

    public static void main(String[] args) throws Exception {

        CrackSingletonByReflectDemo instance1 = CrackSingletonByReflectDemo.getInstance();
        CrackSingletonByReflectDemo instance2 = CrackSingletonByReflectDemo.getInstance();
        // true
        System.out.println(instance1 == instance2);

        // 通过反射的方法破解单例模式
        Class<CrackSingletonByReflectDemo> clazz = (Class<CrackSingletonByReflectDemo>) Class.forName("com.yunhe.dp.ydpodp.p03_singleton_pattern.CrackSingletonByReflectDemo");
        // 获得无参构造器
        Constructor<CrackSingletonByReflectDemo> constructor = clazz.getDeclaredConstructor(null);
        // 设置跳过权限检查,可以访问私有成员
        constructor.setAccessible(true);

        CrackSingletonByReflectDemo instance3 = constructor.newInstance();
        CrackSingletonByReflectDemo instance4 = constructor.newInstance();
        // false
        System.out.println(instance3 == instance4);

    }

}

解决办法:改写私有的构造方法

private CrackSingletonByReflectDemo() {
    if (instance != null) {
        throw new RuntimeException("instance已经实例化!");
    }
}

3.2 通过反序列化破解单例模式

public class CrackSingletonBySerializableDemo implements Serializable {

    private static CrackSingletonBySerializableDemo instance;

    private CrackSingletonBySerializableDemo() {}

    public static synchronized CrackSingletonBySerializableDemo getInstance() {
        if (instance == null) {
            instance = new CrackSingletonBySerializableDemo();
        }
        return instance;
    }

    public static void main(String[] args) throws Exception {

        CrackSingletonBySerializableDemo instance1 = CrackSingletonBySerializableDemo.getInstance();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("instance.txt"));
        out.writeObject(instance1);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("instance.txt"));
        CrackSingletonBySerializableDemo instance2 = (CrackSingletonBySerializableDemo) in.readObject();
        // false
        System.out.println(instance1 == instance2);
        in.close();

    }


}

解决办法:在类中添加readResolve()这个方法,反序列化直接调用readResolve()返回指定的对象,不需要再创建新对象

public Object readResolve() {
    return instance;
}

本文第1节原书:《您的设计模式》作者:CBF4LIFE