接口也可以创建对象吗_单例模式,你真的写对了吗?

作者:何甜甜在吗来源:juejin.im/post/5d8cc45ae51d4577ef53de12

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

本文大纲

  • 什么是单例模式
  • 饿汉式创建单例对象
  • 懒汉式创建单例对象
  • 单例模式的优缺点
  • 单例模式的应用场景

什么是单例模式

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

饿汉式创建单例模式

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

public class HungrySingleton { private static HungrySingleton hungrySingleton = new HungrySingleton(); /** * 私有构造函数,不能被外部所访问 */ private HungrySingleton() {} /** * 返回单例对象 * */ public static HungrySingleton getHungrySingleton() { return hungrySingleton; }}

说明:

  • 构造函数私有化,保证外部不能调用构造函数创建对象,创建对象的行为只能由这个类决定
  • 只能通过getHungrySingleton方法获取对象
  • HungrySingleton对象已经创建完成【在类加载时创建】

缺点:

  • 如果getHungrySingleton一直没有被使用到,有点浪费资源

优点:

  • 由ClassLoad保证线程安全

懒汉式创建单例模式

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

存在错误的懒汉式创建单例对象 根据定义很容易在上面饿汉式的基础上进行修改

public class LazySingleton { private static LazySingleton lazySingleton = null; /** * 构造函数私有化 * */ private LazySingleton() { } private static LazySingleton getLazySingleton() { if (lazySingleton == null) { return new LazySingleton(); } return lazySingleton; }}

说明:

  • 构造函数私有化
  • 当需要时【getLazySingleton方法调用时】才创建

嗯,好像没什么问题,但是当有多个线程同时调用getLazySingleton方法时,此时刚好对象没有初始化,两个线程同时通过lazySingleton == null的校验,将会创建两个LazySingleton对象。必须搞点手段使getLazySingleton方法是线程安全的

synchronize或Lock

很容易想到使用synchronize或Lock对方法进行加锁 使用synchronize:

62c237ed3be5a79d366c7870008585f6.png
fe5320a19919d7da0e57b00197ed03a3.png

这两种方式虽然保证了线程安全,但是性能较差,因为线程不安全主要是由这段代码引起的:

if (lazyLockSingleton == null) { lazyLockSingleton = new LazyLockSingleton();}

给方法加锁无论对象是否已经初始化都会造成线程阻塞。如果对象为null的情况下才进行加锁,对象不为null的时候则不进行加锁,那么性能将会得到提升,双重锁检查可以实现这个需求

双重锁检查

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

8a2328beff543c96bd00a992eec3468f.png

说明:

为什么需要对lazyDoubleCheckSingleton添加volatile修饰符

因为lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();不是原子性的,分为三步:

  • 为lazyDoubleCheckSingleton分配内存
  • 调用构造函数进行初始化
  • 将lazyDoubleCheckSingleton对象指向分配的内存【执行完这步lazyDoubleCheckSingleton将不为null】

为了提高程序的运行效率,编译器会进行一个指令重排,步骤2和步骤三进行了重排,线程1先执行了步骤一和步骤三,执行完后,lazyDoubleCheckSingleton不为null,此时线程2执行到if (lazyDoubleCheckSingleton == null),线程2将可能直接返回未正确进行初始化的lazyDoubleCheckSingleton对象。

出错的原因主要是lazyDoubleCheckSingleton未正确初始化完成【写】,但是其他线程已经读取lazyDoubleCheckSingleton的值【读】,使用volatile可以禁止指令重排序,通过内存屏障保证写操作之前不会调用读操作【执行if (lazyDoubleCheckSingleton == null)】

缺点:

  • 为了保证线程安全,代码不够优雅过于臃肿

静态内部类

public class LazyStaticSingleton { /** * 静态内部类 * */ private static class LazyStaticSingletonHolder { private static LazyStaticSingleton lazyStaticSingleton = new LazyStaticSingleton(); } /** * 构造函数私有化 * */ private LazyStaticSingleton() { } public static LazyStaticSingleton getLazyStaticSingleton() { return LazyStaticSingletonHolder.lazyStaticSingleton; }}

静态内部类在调用时才会进行初始化,因此是懒汉式的,LazyStaticSingleton lazyStaticSingleton = new LazyStaticSingleton();看似是饿汉式的,但是只有调用getLazyStaticSingleton时才会进行初始化,线程安全由ClassLoad保证,不用思考怎么加锁

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

反射攻击:

public class ReflectAttackTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //静态内部类 LazyStaticSingleton lazyStaticSingleton = LazyStaticSingleton.getLazyStaticSingleton(); //通过反射创建LazyStaticSingleton Constructor constructor = LazyStaticSingleton.class.getDeclaredConstructor(); constructor.setAccessible(true); LazyStaticSingleton lazyStaticSingleton1 = constructor.newInstance(); //打印结果为false,说明又创建了一个新对象 System.out.println(lazyStaticSingleton == lazyStaticSingleton1); //synchronize LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton(); Constructor lazySynchronizeSingletonConstructor = LazySynchronizeSingleton.class.getDeclaredConstructor(); lazySynchronizeSingletonConstructor.setAccessible(true); LazySynchronizeSingleton lazySynchronizeSingleton1 = lazySynchronizeSingletonConstructor.newInstance(); System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1); //lock LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton(); Constructor lazyLockSingletonConstructor = LazyLockSingleton.class.getConstructor(); lazyLockSingletonConstructor.setAccessible(true); LazyLockSingleton lazyLockSingleton1 = lazyLockSingletonConstructor.newInstance(); System.out.println(lazyLockSingleton == lazyLockSingleton1); //双重锁检查 LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton(); Constructor lazyDoubleCheckSingletonConstructor = LazyDoubleCheckSingleton.class.getConstructor(); lazyDoubleCheckSingletonConstructor.setAccessible(true); LazyDoubleCheckSingleton lazyDoubleCheckSingleton1 = lazyDoubleCheckSingletonConstructor.newInstance(); System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton1); }}

基于静态内部类和基于synchronize加锁创建单例对象的方式都可以通过反射的方式创建新对象,存在反射攻击,其余几种创建单例对象的方式使用反射创建新对象将会报错。针对存在的反射攻击根据网上提供的思路在抢救一下,抢救姿势如下:

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

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

//调用反射前将flag设置为falseField 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的代码可知

上述实现单例对象的方式既要考虑线程安全、又要考虑攻击,而通过枚举创建单例对象完全不用担心这些问题

枚举

public enum EnumSingleton { INSTANCE; public static EnumSingleton getEnumSingleton() { return INSTANCE; }}

代码实现也相当优美,总共才8行代

实现原理:枚举类的域(field)其实是相应的enum类型的一个实例对象

可以参考:

https://stackoverflow.com/questions/26285520/implementing-singleton-with-an-enum-in-java

枚举攻击测试:

public class EnumAttackTest { public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { EnumSingleton enumSingleton = EnumSingleton.getEnumSingleton(); //序列化攻击 byte[] serialize4 = SerializationUtils.serialize(enumSingleton); EnumSingleton enumSingleton2 = SerializationUtils.deserialize(serialize4); System.out.println(enumSingleton == enumSingleton2); //反射攻击 Constructor enumSingletonConstructor = EnumSingleton.class.getConstructor(); enumSingletonConstructor.setAccessible(true); EnumSingleton enumSingleton1 = enumSingletonConstructor.newInstance(); System.out.println(enumSingleton == enumSingleton1); }}

反射攻击将会抛出异常,序列化攻击对它无效,打印结果为true,用枚举创建单例对象真的是无懈可击

单例模式的优点

  • 只创建了一个实例,节省内存开销
  • 减少了系统的性能开销,创建对象回收对象对性能都有一定的影响
  • 避免对资源的多重占用
  • 在系统设置全局的访问点,优化和共享资源优化

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

单例模式的缺点

  • 不适用于变化的对象
  • 单例模式中没有抽象层,扩展有困难
  • 与单一原则冲突。一个类应该只实现一个逻辑,而不关心它是否单例,是不是单例应该由业务决定

单例模式的应用场景

  • Spring IOC默认使用单例模式创建bean
  • 创建对象需要消耗的资源过多时
  • 需要定义大量的静态常量和静态方法的环境,比如工具类【感觉是最常见应用场景】

小结

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

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

https://github.com/TiantianUpup/design-patterns

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值