Android开发Android开发经验谈Android技术知识

单例模式,你真的写对了吗?

2021-12-09  本文已影响0人  i小灰

看公司代码的时候发现项目中单例模式应用挺多的,并且发现的两处单例模式用的还是不同的方式实现的,那么单例模式到底有几种写法呢?单例模式看似很简单,但是实际写起来却问题多多

本文大纲

什么是单例模式

确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例,并且有两种创建方式,一种是饿汉式创建,另外一种是懒汉式创建

饿汉式创建单例模式

饿汉式创建就是在类加载时就已创建好对象,而不是在需要时在创建对象

public class HungrySingleton {
    private static HungrySingleton hungrySingleton = new HungrySingleton();

    /**
     * 私有构造函数,不能被外部所访问
     */
    private HungrySingleton() {}

    /**
     * 返回单例对象
     * */
    public static HungrySingleton getHungrySingleton() {
        return hungrySingleton;
    }
}

说明:

缺点:

优点:

懒汉式创建单例模式

懒汉式创建就是在第一次需要该对象时在创建

在加锁之前先判断lazyDoubleCheckSingleton == null是否成立,如果不成立直接返回创建好的对象,成立在加锁

public class LazyDoubleCheckSingleton {
    /**
     * 使用volatile进行修饰,禁止指令重排
     * */
    private static volatile LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

    /**
     * 构造函数私有化
     * */
    private LazyDoubleCheckSingleton() {
    }

    public static LazyDoubleCheckSingleton getLazyDoubleCheckSingleton() {
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }

        return lazyDoubleCheckSingleton;
    }
}

说明:

缺点:

前面几种方式实现单例的方式虽然各有优缺点,但是基本实现了单例线程安全的要求。但是总有人看不惯单例模式勤俭节约的优点,对它进行攻击。对它进行攻击无非就是创建不只一个类,java中创建对象的方式有newclone、序列化、反射。构造函数私有化不可能通过new创建对象、同时单例类没有实现Cloneable接口无法通过clone方法创建对象,那剩下的攻击只有反射攻击和序列化攻击了
反射攻击:

public class ReflectAttackTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //静态内部类
        LazyStaticSingleton lazyStaticSingleton = LazyStaticSingleton.getLazyStaticSingleton();
        //通过反射创建LazyStaticSingleton
        Constructor<LazyStaticSingleton> constructor = LazyStaticSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazyStaticSingleton lazyStaticSingleton1 = constructor.newInstance();
        //打印结果为false,说明又创建了一个新对象
        System.out.println(lazyStaticSingleton == lazyStaticSingleton1);

        //synchronize
        LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton();
        Constructor<LazySynchronizeSingleton> lazySynchronizeSingletonConstructor = LazySynchronizeSingleton.class.getDeclaredConstructor();
        lazySynchronizeSingletonConstructor.setAccessible(true);
        LazySynchronizeSingleton lazySynchronizeSingleton1 = lazySynchronizeSingletonConstructor.newInstance();
        System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1);

        //lock
        LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton();
        Constructor<LazyLockSingleton> lazyLockSingletonConstructor = LazyLockSingleton.class.getDeclaredConstructor();
        lazyLockSingletonConstructor.setAccessible(true);
        LazyLockSingleton lazyLockSingleton1 = lazyLockSingletonConstructor.newInstance();
        System.out.println(lazyLockSingleton == lazyLockSingleton1);

        //双重锁检查
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton();
        Constructor<LazyDoubleCheckSingleton> lazyDoubleCheckSingletonConstructor = LazyDoubleCheckSingleton.class.getDeclaredConstructor();
        lazyDoubleCheckSingletonConstructor.setAccessible(true);
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton1 = lazyDoubleCheckSingletonConstructor.newInstance();
        System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton1);
    }
}

都存在反射攻击,都可以创建出一个新对象,打印结果都为false。针对存在的反射攻击根据网上提供的思路在抢救一下,抢救姿势如下:

 private LazySynchronizeSingleton() {
      //flag为线程间共享,进行加锁控制
      synchronized (LazySynchronizeSingleton.class) {
          if (flag == false) {
              flag = !flag;
          } else {
              throw new RuntimeException("单例模式被攻击");
          }
      }
  }

构造函数只能调用一次,调用第二次将抛出异常,通过flag来判断构造函数是否已经被调用过一次了。但是我们仍可以通过反射修改flag的值:

//调用反射前将flag设置为false
Field flagField = lazySynchronizeSingleton.getClass().getDeclaredField("flag");
flagField.setAccessible(true);
flagField.set(lazySynchronizeSingleton, false);

抢救失败,你可能想通过final修饰禁止修改,但是反射可以先去除final,在加上final修改值,对于反射攻击,无力回天,只能选择不适用存在反射攻击的单例创建方式

反序列化攻击:

public class SerializableAttackTest {
    public static void main(String[] args) {
        //懒汉式
        HungrySingleton hungrySingleton = HungrySingleton.getHungrySingleton();
        //序列化
        byte[] serialize = SerializationUtils.serialize(hungrySingleton);
        //反序列化
        HungrySingleton hungrySingleton1 = SerializationUtils.deserialize(serialize);
        System.out.println(hungrySingleton == hungrySingleton1);

        //双重锁
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton();
        byte[] serialize1 = SerializationUtils.serialize(lazyDoubleCheckSingleton);
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton11 = SerializationUtils.deserialize(serialize1);
        System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton11);

        //lock
        LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton();
        byte[] serialize2 = SerializationUtils.serialize(lazyLockSingleton);
        LazyLockSingleton lazyLockSingleton1 = SerializationUtils.deserialize(serialize2);
        System.out.println(lazyLockSingleton == lazyLockSingleton1);

        //synchronie
        LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton();
        byte[] serialize3 = SerializationUtils.serialize(lazySynchronizeSingleton);
        LazySynchronizeSingleton lazySynchronizeSingleton1 = SerializationUtils.deserialize(serialize3);
        System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1);

        //静态内部类
        LazyStaticSingleton lazyStaticSingleton = LazyStaticSingleton.getLazyStaticSingleton();
        byte[] serialize4 = SerializationUtils.serialize(lazySynchronizeSingleton);
        LazyStaticSingleton lazyStaticSingleton1 = SerializationUtils.deserialize(serialize4);
        System.out.println(lazyStaticSingleton == lazyStaticSingleton1);

    }
}

打印结果都为false,都存在反序列化攻击
对于反序列化攻击,还是有有效的抢救方式的,抢救姿势如下:

private Object readResolve() {
    return lazySynchronizeSingleton;
}
复制代码

添加readResolve方法并返回创建的单例对象,至于抢救的原理,可以通过跟进SerializationUtils.deserialize的代码可知
上述实现单例对象的方式既要考虑线程安全、又要考虑攻击,而通过枚举创建单例对象完全不用担心这些问题

单例模式的优点

总结一下就是节约资源、提升性能

单例模式的缺点

单例模式的应用场景

小结

总共介绍了六种正确创建单例对象的方式,推荐使用饿汉式创建单例对象的方式,如果对资源使用有要求,则推荐使用静态内部类【注意反序列化攻击】,其他方式在保证线程安全的同时对性能将会有影响。枚举类其实是非常不错的,线程安全、不存在反射攻击和反序列化攻击,但是感觉这种创建单例方式应用较少,公司代码中使用的是双重锁检查和静态内部类【存在反序列化攻击】创建单例方式,甚至之前出去面试时面试官让写一个单例,我使用的是枚举方式,面试官都不知道有这种方式

附:完整例子代码+测试代码

上一篇 下一篇

猜你喜欢

热点阅读