您的位置:首页 > 编程语言 > Go语言

GOF23设计模式-单例模式-5种实现方式比较和防止反射与反序列化漏洞

2019-07-21 17:44 393 查看

设计模式GOF23-单例模式5种方式写法与比较

  • 常见的五种单例模式实现方式
  • 如何选用?
  • 常见五种单例模式在多线程环境下的效率测试
  • 通过反射破解以上单例模式及如何防止(不包含枚举式)
  • 通过反序列化破解以上单例模式及如何防止(不包含枚举式)
  • 设计模式

    学习设计模式,就是将设计者的思维融入学习和工作中,更高层次的思考,而不是死记硬背模式代码。

    设计模式的分类

    GOF将设计模式分为以下三个模块:

    1. 创建型模式: 单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式;
    2. 结构型模式: 适配器模式、桥接模式、组合模式、装饰模式、外观模式、享元模式、代理模式;
    3. 行为型模式: 模板方法模式、命令模式、迭代器模式、观察者模式、中介模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式;

    单例模式

    核心作用

    保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。

    常见应用场景

    1. Windows的Task Manager(任务管理器)就是很典型的单例模式
    2. Windows的Recycle Bin(回收站)也是很典型的单例模式,在整个系统运行过程中,回收站一直维护着仅有的一个实例。
    3. 网站的计数器,一般也采用单例模式实现,否则难以同步。
    4. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
    5. 在servlet编程中,每个servlet 也是单例。
    6. Application也是单例的典型应用(Servlet编程中有涉及到)。
    7. 应用程序的日志应用,一般都采用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
    8. 项目中读取配置文件的类,一般也只有一个对象。没必要每次使用配置文件信息数据,都去new一个新的对象去读取。
    9. 数据库连接池的设计一般也是采用单例模式。数据库连接是一种数据库资源。
    10. 在Spring中,每个Bean默认就是单例的,这样做的优点是方便Spring容器的管理。
    11. 在Spring MVC框架/Struts1框架中,控制器对象也是单例。

    单例模式的优点

    1. 由于单例模式只生成了一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置文件、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
    2. 单例模式可以在系统设置全局的访问点,优化共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

    常见的五种单例模式实现方式

    • 主要: 饿汉式(线程安全,调用效率高。但是不能延时加载。)
    • 懒汉式(线程安全,调用效率不高。但是可以延时加载。)
  • 其他:
      双重检测锁式(由于JVM底层内部模型原因,偶尔出现问题。不建议使用。)
    • 静态内部类式(线程安全,调用效率高。可以延时加载。)
    • 枚举单例(线程安全,调用效率高,不能延时加载。)

    实现方式1:饿汉式(单例对象立即加载)

    实现代码

    package com.tumbler.singleton;
    
    /**
    * User:tumbler
    * Desc:单例模式:饿汉式
    */
    public class SingletonDemo1 {
    //类初始化时立即加载此对象,天然线程安全,没有延时加载
    private static SingletonDemo1 instance = new SingletonDemo1();
    
    //构造方法私有化
    private SingletonDemo1(){}
    
    //提供外部访问实例方法,方法没有同步,调用效率高
    public static SingletonDemo1 getInstance(){
    return instance;
    }
    }

    测试

    package com.tumbler.singleton;
    
    /**
    * User:tumbler
    * Desc:测试单例模式
    */
    public class Client {
    public static void main(String[] args){
    SingletonDemo1 instance1 = SingletonDemo1.getInstance();
    SingletonDemo1 instance2 = SingletonDemo1.getInstance();
    System.out.println(instance1 == instance2); // true
    }
    }

    分析

    • 饿汉式单例模式代码中,static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此可以省略synchronized关键字。
    • 问题:如果只是加载本类,而不是要调用getInstance(),甚至永远也没有调用,则会造成资源浪费!
      UML

    实现方式2:懒汉式(单例对象延迟加载)

    代码实现

    package com.tumbler.singleton;
    
    /**
    * User:tumbler
    * Desc:单例模式:懒汉式
    */
    public class SingletonDemo2 {
    //类初始化时不初始化此对象,延时加载
    private static SingletonDemo2 instance;
    
    //私有化构造器
    private SingletonDemo2() {}
    
    //方法同步,调用效率低
    public static synchronized SingletonDemo2 getInstance(){
    if (instance == null) {
    instance = new SingletonDemo2();
    }
    return instance;
    }
    }

    测试

    package com.tumbler.singleton;
    
    /**
    * User:tumbler
    * Desc:测试单例模式
    */
    public class Client {
    public static void main(String[] args){
    SingletonDemo2 instance1 = SingletonDemo2.getInstance();
    SingletonDemo2 instance2 = SingletonDemo2.getInstance();
    System.out.println(instance1 == instance2); // true
    }
    }

    分析

    • 懒加载(延时加载),真正用的时候再去加载。
    • 问题:资源利用率高了,但是每次调用getInstance()方法都要同步,并发效率低了。
      UML

    实现方式3:双重检测锁(由于JVM内部模型偶尔出现问题,不建议使用)

    代码实现

    package com.tumbler.singleton;
    
    /**
    * User:tumbler
    * Desc:单例模式:双重校验锁
    */
    public class SingletonDemo3 {
    private volatile static SingletonDemo3 instance;
    private SingletonDemo3() {}
    public static SingletonDemo3 getInstance(){
    if (instance == null) {
    synchronized (SingletonDemo3.class) {
    if (instance == null) {
    instance = new SingletonDemo3();
    }
    }
    }
    return instance;
    }
    }

    测试

    package com.tumbler.singleton;
    
    /**
    * User:tumbler
    * Desc:测试单例模式
    */
    public class Client {
    public static void main(String[] args){
    SingletonDemo3 instance1 = SingletonDemo3.getInstance();
    SingletonDemo3 instance2 = SingletonDemo3.getInstance();
    System.out.println(instance1 == instance2); // true
    }
    }

    分析

    • 此模式将同步内容下放到if内部,提高了执行效率,不必每次获取对象时都进行同步,只有第一次才同步,实例创建后就没必要了。
    • 问题:由于编译器优化原因和JVM内部模型原因,偶尔会出现问题,所以不建议使用。
    • instance 采用 volatile 修饰是很有必要的,因为 instance = new SingletonDemo3() 这句话可以分为三步:
        为 instance 分配内存空间;
      1. 初始化 instance ;
      2. 将 instance 指向分配的内存空间。
        但是由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。 指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得一个未初始化的实例。例如:线程T1执行了1和3,此时T2调用 getInstance() 后发现 instance 不为空,因此返回 instance , 但是此时的 instance 还没有被初始化。
        使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行。
        UML

    实现方式4:静态内部类(也是一种延迟加载方式)

    代码

    package com.tumbler.singleton;
    
    /**
    * User:tumbler
    * Desc:单例模式:静态内部类
    */
    public class SingletonDemo4 {
    private static class SingletonClassInstance {
    private static final SingletonDemo4 instance = new SingletonDemo4();
    }
    private SingletonDemo4() {}
    
    public static SingletonDemo4 getInstance() {
    return SingletonClassInstance.instance;
    }
    }

    测试

    package com.tumbler.singleton;
    
    /**
    * User:tumbler
    * Desc:测试单例模式
    */
    public class Client {
    public static void main(String[] args){
    SingletonDemo4 instance1 = SingletonDemo4.getInstance();
    SingletonDemo4 instance2 = SingletonDemo4.getInstance();
    System.out.println(instance1 == instance2); // true
    }
    }

    分析

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

    实现方式5:枚举类实现(实现简单,无是延迟加载)

    代码

    package com.tumbler.singleton;
    
    /**
    * User:tumbler
    * Desc:单例模式:枚举实现
    */
    public enum  SingletonDemo5 {
    //定义一个枚举元素,它就代表了一个SingletonDemo的对象
    INSTANCE;
    /**
    * 单例可以有自己的操作
    */
    public void singletonOperation() {
    //功能处理
    }
    }

    测试

    package com.tumbler.singleton;
    
    /**
    * User:tumbler
    * Desc:测试单例模式
    */
    public class Client {
    public static void main(String[] args){
    SingletonDemo5 instance1 = SingletonDemo5.INSTANCE;
    SingletonDemo5 instance2 = SingletonDemo5.INSTANCE;
    System.out.println(instance1 == instance2); // true
    }
    }

    分析

    • 优点:实现简单。枚举本身就是单例模式。由JVM从根本上提供了保障,避免通过反射和反序列化的漏洞。
    • 缺点:无延迟加载。
      UML

    如何选用?

    • 单例对象,占用资源少、不需延迟加载:枚举式 好于 饿汉式;
    • 单例对象,占用资源大、需要延迟加载:静态内部类式 好于 懒汉式。

    常见五种单例模式在多线程环境下的效率测试

    使用CountDownLatch:同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

    • countDown():当前线程调用此方法,则计数减一(建议放到finally里执行)。
    • await():调用此方法会一直阻塞当前线程,直到计时器的值为0。

    下面是我测试的结果(10个线程调用1000000次),大家关注相对值即可,不同环境下的程序测试值完全不一样。

    设计模式 时间(ms)
    饿汉式 71
    懒汉式 455
    静态内部类式 77
    双重校验锁式 83
    枚举式 84

    测试代码示例

    package com.tumbler.singleton;
    
    import java.util.concurrent.CountDownLatch;
    
    /**
    * User:tumbler
    * Desc:测试比较五种方式单例模式的效率
    */
    public class TestSingletonDemo {
    public static void main(String[] args) throws Exception{
    
    long start = System.currentTimeMillis();
    int threadNum = 10;
    final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
    
    for (int i = 0; i < threadNum; i++) {
    new Thread(() -> {
    for (int j = 0; j < 1000000; j++) {
    //Object o = SingletonDemo1.getInstance();
    Object o = SingletonDemo5.INSTANCE;
    }
    countDownLatch.countDown();
    }).start();
    }
    countDownLatch.await();
    long end = System.currentTimeMillis();
    
    System.out.println("总耗时:" + (end - start));
    }
    }

    通过反射破解以上单例模式及如何防止(不包含枚举式)

    测试代码

    package com.tumbler.singleton;
    
    import java.lang.reflect.Constructor;
    
    /**
    * User:tumbler
    * Desc:测试反射破坏单例模式,以懒汉式为例
    */
    public class TestReflex {
    public static void main(String[] args) throws Exception {
    SingletonDemo2 instance1 = SingletonDemo2.getInstance();
    SingletonDemo2 instance2 = SingletonDemo2.getInstance();
    System.out.println(instance1 == instance2); // true  单例
    
    //通过反射破坏单例模式
    Class<SingletonDemo2> clazz = (Class<SingletonDemo2>) Class.forName("com.tumbler.singleton.SingletonDemo2");
    Constructor<SingletonDemo2> constructor = clazz.getDeclaredConstructor(null);
    constructor.setAccessible(true); // 访问私有构造器
    SingletonDemo2 instance3 = constructor.newInstance();
    SingletonDemo2 instance4 = constructor.newInstance();
    System.out.println(instance3 == instance4); //false  单例被破坏
    }
    }

    如何防止?
    在私有构造器内抛出异常,即多次构造直接报错阻止。
    修改SingletonDemo2:

    package com.tumbler.singleton;
    
    /**
    * User:tumbler
    * Desc:单例模式:懒汉式  防止反射破坏单例
    */
    public class SingletonDemo2 {
    //类初始化时不初始化此对象,延时加载
    private static SingletonDemo2 instance;
    
    //私有化构造器
    private SingletonDemo2() {
    if(instance != null) {
    throw new RuntimeException();
    }
    }
    
    //方法同步,调用效率低
    public static synchronized SingletonDemo2 getInstance(){
    if (instance == null) {
    instance = new SingletonDemo2();
    }
    return instance;
    }
    }

    通过反序列化破解以上单例模式及如何防止(不包含枚举式)

    要是用序列化,则先给SingletonDemo2实现序列化接口

    public class SingletonDemo2 implements Serializable

    测试代码

    package com.tumbler.singleton;
    
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    
    /**
    * User:tumbler
    * Desc:通过反序列化破坏单例模式
    */
    public class TestDeserialize {
    public static void main(String[] args) throws Exception{
    SingletonDemo2 instance1 = SingletonDemo2.getInstance();
    SingletonDemo2 instance2 = SingletonDemo2.getInstance();
    System.out.println(instance1 == instance2); // true  单例
    
    //序列化
    FileOutputStream fos = new FileOutputStream("D:/a.txt");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    oos.writeObject(instance1);
    oos.close();
    fos.close();
    
    //反序列化
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/a.txt"));
    SingletonDemo2 instance3 = (SingletonDemo2) ois.readObject();
    System.out.println(instance1 == instance3); // false 破坏单例
    }
    }

    如何防止
    通过定义readResolve()方法阻止破坏。在SingletonDemo2 添加以下方法即可:

    //反序列化
    private Object readResolve() throws ObjectStreamException {
    return instance;
    }
  • 内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: