8种常用的设计模式(3-2) —— 单例模式

    科技2024-10-04  19

    目录

    1.单例模式1.饿汉式单例2.懒汉式单例1.普通的懒汉式单例2.DCL懒汉式单例 3.静态内部类实现单例模式 2.枚举1.使用反射破坏单例模式2.使用枚举防止单例被破坏1.枚举怎么防止反射搞破坏2.使用枚举改造单例模式

    1.单例模式

    1.饿汉式单例

    饿汉式,顾名思义,饥不择食,上来就要public class Hungry { private Hungry(){} //单例模式标志,私有构造 private static final Hungry HUNGRY = new Hungry(); //饿汉式,上来直接new一个对象放在这供使用 public static Hungry getInstance(){ //返回实例的方法 return HUNGRY; } } 饿汉式的缺点:浪费资源原因:因为饿汉式上来就new了一个静态对象,所以不管我们用不用,它都已经存在在内存中了,那么如果这个对象中有很多占用内存的成员方法,比如: public class Hungry { private byte[] data1 = new byte[1024*1024]; private byte[] data2 = new byte[1024*1024]; private byte[] data3 = new byte[1024*1024]; private byte[] data4 = new byte[1024*1024]; private Hungry(){} //单例模式标志,私有构造 private static final Hungry HUNGRY = new Hungry(); //饿汉式,上来直接new一个对象放在这供使用 public static Hungry getInstance(){ //返回实例的方法 return HUNGRY; } }

    在饿汉式的这种上来就new,在不使用它的时候就会比较占用资源的情况下,提出了懒汉式单例模式解决该问题

    2.懒汉式单例

    1.普通的懒汉式单例

    懒汉式,顾名思义,就是这个玩意儿它不勤快,很懒惰;该它造的东西,它非要推拖到你正要使用的时候它才去给你造public class Lazy { private Lazy(){} //私有化构造,单例模式标志,不能变 private static Lazy LAZY; //单例对象的引用先定义出来,注意不能使用final关键字,final是用来定义常量的,所以定义常量的时候就要求你赋值,并且不可改变,所以饿汉式需要使用final public static Lazy getInstance(){ if (LAZY==null){ //判断LAZY是否已经实例化了,没有就将它实例化 LAZY = new Lazy(); } return LAZY; //如果lazy已经实例化了就直接将它返回 } } 虽然懒汉式单例解决了饿汉式单例的资源浪费问题,但是在实际的使用过程中,我们发现:懒汉式对于多线程支持并不好,甚至是不支持

    2.DCL懒汉式单例

    DCL就是双重锁🔒的意思,就是在线程获取对象的锁的前后都加上对于单例实例是否为null的判断,使用双重锁的意义在于节约系统开销提供运行性能/效率 第一层🔒的意义:在单例对象实例化之后不再需要其他线程进加🔒再来判断单例对象是否为null第二层🔒的意义:在第一次new单例对象之前,可能出现多个线程同时通过第一层🔒的情况,但是由于通过第一层🔒之后就要排队+获取对象🔒,所以加上第二层判断可以在单例对象new完之后,保障其他线程不再new新的线程通过两层加🔒/if判断,就实现了整个流程只会new一个单例对象的效果
    多线程中的单例public class Lazy { private Lazy(){ System.out.println(Thread.currentThread().getName()+"使用单例,并成功new 了一个LAZY对象"); } //私有化构造,单例模式标志,不能变 private static Lazy LAZY; //单例对象的引用先定义出来,注意不能使用final关键字,final是用来定义常量的,所以定义常量的时候就要求你赋值,并且不可改变,所以饿汉式需要使用final public static Lazy getInstance(){ if (LAZY==null){ //判断LAZY是否已经实例化了,没有就将它实例化 LAZY = new Lazy(); } return LAZY; //如果lazy已经实例化了就直接将它返回 } public static void main(String[] args) { for (int i=0;i<10;i++){ new Thread(()->{ LAZY.getInstance(); }).start(); } } } 从上面的运行结果我们就可以看出多线程中的单例模式不可靠,或者说不再是单例模式
    回顾多线程解决上面懒汉式单例多线程不靠谱的办法就是使用线程同步,或者说加🔒,加上锁之后多个线程对于对象资源的访问就会排队,这就可以保证懒汉式单例中的对象只会被实例化一次正常的情况下,我们直接使用关键字synchronized将它加在方法getInstance()上面即可解决问题,但是加锁的操作是很影响程序性能的,由于同步一个方法会降低100倍或更高的性能,而每个线程在调用getInstance()都会获取类的🔒,所以这是很消耗内存资源的;并且单例对象一旦初始化完成,获取和释放锁就显得很不必要,即一旦一个线程new 了单例对象,后面的程序再来获取🔒并进行对象是否为空的判断很没有必要,所以出现了"双重检查锁定模式"解决办法就是:我们不使用同步方法,而使用同步块,将多个线程访问的对象LAZY锁起来,然后进入正常的判断单例对象引用是否为null;前面的操作只是将同步方法变成了同步块,而双重检查锁定模式还要在同步块外面加上一个判断单例对象引用是否为null的判断,即在线程获取对象🔒之前,先判断单例对象是不是已经实例化了,如果已经实例化了,就不用再获取🔒和释放🔒,而直接返回单例对象实例public static Lazy getInstance(){ if (LAZY==null){ //线程进来首先判断单例对象是否已经实例化,实例化了就不用再加锁进行判断了 synchronized(Lazy.class){ if (LAZY==null){ LAZY = new Lazy(); } } } return LAZY; //如果lazy已经实例化了就直接将它返回 } 注意:第一次if判断是在避免不必要的加🔒和解🔒带来的内存占用,第二层if判断的作用在于:在单例对象被实例化之前,有1个以上的多线程通过了第一次检查,由于使用了同步块,所以它们需要排队进入同步块执行,那么第二个if判断的作用就体现在这里,即只要有一个线程通过了第二层的if判断,它就会实例化单例对象,这就让后面再进入同步块的线程不能再去实例化新的单例对象了所以,第一次if判断是在过滤不必要的加锁和解锁对于程序性能的影响;第二层if判断是在过滤刚开始一起通过了第一层if判断的线程不能多次new单例对象除了上面的使用两层if之外,我们还需要使用关键字volatile去关闭我们的单例对象的指令重排,原因就是:new一个对象的正常步骤为 ①为引用分配内存空间,此时对象的引用就不为空了,而是为这个内存地址②执行构造,初始化对象③将对象的地址传给对象的引用,就是将对象的地址存入引用执行的那片内存空间 但是在实例执行的时候,线程可能不是按照这样的顺序在执行,它可能的另一种顺序就是①③②,这就叫"指令重排";这样的顺序对于new对象的这个线程没有什么影响,在单线程中也没什么影响,但是在多线程的懒汉式单例模式下就会出现问题因为懒汉式单例只要判断单例对象的引用不为null,就认为这个对象已经创建好了,它就会直接返回对象的引用并在线程中调用对象的方法;如果线程A实例化单例对象采用的是①③②的顺序进行实例化,那么在进行了③但是还没进行②之前,线程B看到的单例对象的引用不为空,就会直接调用,但是实际上这片内存空间中什么都没有,就会出现空指针异常,所以我们需要关闭new单例对象的时候的指令重排,方式就是在对象的引用处使用关键字volatile即可,即使得这个引用的赋值过程变成一个原子性操作,不能变更public class Lazy { private Lazy(){ System.out.println(Thread.currentThread().getName()+"使用单例,并成功new 了一个LAZY对象"); } //私有化构造,单例模式标志,不能变 private volatile static Lazy LAZY; //单例对象的引用先定义出来,注意不能使用final关键字,final是用来定义常量的,所以定义常量的时候就要求你赋值,并且不可改变,所以饿汉式需要使用final public static Lazy getInstance(){ if (LAZY==null){ //线程进来首先判断单例对象是否已经实例化,实例化了就不用再加锁进行判断了 synchronized(Lazy.class){ if (LAZY==null){ LAZY = new Lazy(); } } } return LAZY; //如果lazy已经实例化了就直接将它返回 } public static void main(String[] args) { for (int i=0;i<10;i++){ new Thread(()->{ LAZY.getInstance(); }).start(); } } }

    只有双重检测🔒+volatile关键字,才是真正解决懒汉式单例的完整且正确的方法参考博客

    3.静态内部类实现单例模式

    public class StaticInnerClass { private StaticInnerClass(){}//私有构造 public static class InnerClass{ private static final StaticInnerClass STATIC_INNER_CLASS = new StaticInnerClass(); } public static StaticInnerClass getInstance(){ return InnerClass.STATIC_INNER_CLASS; } } 从实现上来看,静态内部类实现其实就和直接使用饿汉式单例模式实现单例是一样的,由于都是静态的,所以上来就会先把对象new出来占用内存

    2.枚举

    由于霸道的反射技术的存在,所以即使我们的单例模式中使用了构造私有,但是反射一样可以破除该访问权限,获得到类的构造器,并且随意的new对象,这显然违反了单例模式的初衷即单例模式并不安全

    1.使用反射破坏单例模式

    除了使用上面的3种方法实现单例模式以外,我们还可以使用反射这个技术就很霸道了,不管你类里面的东西是不是私有的,它都能拿出来使用;所以反射可以破坏我们设计好的懒汉式单例模式public static void main(String[] args) throws Exception { Lazy instance1 = Lazy.getInstance(); System.out.println(instance1); Class<Lazy> lazyClass = Lazy.class; Constructor<Lazy> constructor = lazyClass.getDeclaredConstructor(null); constructor.setAccessible(true);//设置构造器可以获取使用,这就破坏了单例模式的构造器私有 Lazy instance2 = constructor.newInstance(); System.out.println(instance2); } 注意:上面的main()中我完全没有修改原来的代码,只是在main()中调用了反射,获取了单例类的构造,并调用了它获取了一个对象,最后输出两个对象就可以通过对比内存地址,查看两个对象是是否一致 所以,反射破坏了单例怎么解决?在构造中加入非空判断private Lazy(){ synchronized (Lazy.class){ if (LAZY!=null){ throw new RuntimeException("小火鸡,不要使用反射搞破坏"); } } System.out.println(Thread.currentThread().getName()+"使用单例,并成功new 了一个LAZY对象"); } 当LAZY!=null的情况下再来调用构造,直接手动抛出异常 此时我们做的操作就是将双重检查锁升级为了3重检查锁 再来使用反射搞一些破坏,前面我们通过单例的Lazy.getInstance()导致单例中对象非null,此时我们根本就不去调用Lazy.getInstance(),所有的对象都通过反射获取,这样就不会使得单例中对象的引用变为非nullpublic static void main(String[] args) throws Exception { /* Lazy instance1 = Lazy.getInstance(); System.out.println(instance1);*/ Class<Lazy> lazyClass = Lazy.class; Constructor<Lazy> constructor = lazyClass.getDeclaredConstructor(null); constructor.setAccessible(true);//设置这个类禁用访问安全检查,即无效化访问控制符 Lazy instance1 = constructor.newInstance(); System.out.println(instance1); Lazy instance2 = constructor.newInstance(); System.out.println(instance2); }

    【问题解决】定义一个标志位/红绿灯,只要构造被调用一次,就变为false public class Lazy { private static boolean flag = false; private Lazy(){ synchronized (Lazy.class){ if (!this.flag){ this.flag = true; }else { throw new RuntimeException("小火鸡,不要使用反射搞破坏"); } } System.out.println(Thread.currentThread().getName()+"使用单例,并成功new 了一个LAZY对象"); } }

    但是标志位始终还是一个成员属性,我们可以通过反射获取它,在获取一个对象之后又将它修改为false,这样不就又可以破坏单例模式了吗? 可见此时单例模式又被破坏了所以只要有反射,单例模式就存在被破坏的风险,那么怎么解决反射破化单例呢?

    2.使用枚举防止单例被破坏

    怎么才能防止万恶的反射破坏单例模式呢?Java官方给出了解决办法,反射之所以可以获取多个对象,是因为它可以调用方法constructor.newInstance(),我们可以去查看newInstance()的源码@CallerSensitive public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers); } } if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }

    所以,Java官方的办法就是直接对反射获取对象的过程下手,只要我们将代理模式设为枚举类型,那么就可以防止反射破坏我们的单例模式

    1.枚举怎么防止反射搞破坏

    Java中的枚举类型枚举是Java中的一种数据结构,用于定义有限个数的对象的一种数据结构语法:enum 枚举类名称{ 枚举类实例名称列表 }定义枚举类的语法: 枚举类,使用关键字enum enum color{ 枚举类中规定的有限个数的实例 RED,GREEN,BLUE; } 获取枚举类中实例的语法: 枚举类名.实例名称 enum.RED; enum.GREEN; enum.BLUE; 测试反射是否能够破坏枚举类import java.lang.reflect.Constructor; public enum EnumSingle { //定义一个枚举类 INSTANCE; //枚举类提供的一个实例 public static EnumSingle getInstance(){ //获取实例的方法,当然也可以使用枚举类名.实例名称获取 return INSTANCE; } } class Test{ public static void main(String[] args) throws Exception { EnumSingle instance1 = EnumSingle.INSTANCE; //从枚举类中获取实例1 //使用反射获取实例2 Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(null); //获取无参构造 constructor.setAccessible(true); EnumSingle instance2 = constructor.newInstance(); //通过反射获取实例2 System.out.println(instance1==instance2); //判断两个对象引用是否相等,从而判断枚举类是否被反射破坏 } }

    【分析】对于枚举类EnumSingle我们并没有定义一个有参构造来覆盖无参构造,那么我们的无参构造去哪里了?我们可以反编译一下该java文件生成的.class文件(使用专业的反编译软件jad) 所以上面报错没有无参构造的原因就是使用枚举之后,获取的对象不是通过无参构造创建的,而是通过上图的有参构造创建的那么我们可以通过反射获取上面的有参构造来破坏枚举类吗?import java.lang.reflect.Constructor; public enum EnumSingle { //定义一个枚举类 INSTANCE; //枚举类提供的一个实例 public static EnumSingle getInstance(){ //获取实例的方法,当然也可以使用枚举类名.实例名称获取 return INSTANCE; } } class Test{ public static void main(String[] args) throws Exception { EnumSingle instance1 = EnumSingle.INSTANCE; //从枚举类中获取实例1 EnumSingle.INSTANCE.name(); //使用反射获取实例2 Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class); //获取无参构造 constructor.setAccessible(true); EnumSingle instance2 = constructor.newInstance(); //通过反射获取实例2 System.out.println(instance1==instance2); //判断两个对象引用是否相等,从而判断枚举类是否被反射破坏 } }

    通过上面的验证我们可以得出以下两个结论 使用枚举类可以防止反射获取类的构造并创建对象枚举类中的对象都是通过一个有参构造创建出来的,而不是一个无参构造

    2.使用枚举改造单例模式

    所以我们要实现真正意义上安全不会被反射搞破坏的单例模式,就直接使用枚举定义要使用单例的类即可public enum EnumSingle { //定义一个枚举类 INSTANCE; //枚举类提供的一个实例 public static EnumSingle getInstance(){ //获取实例的方法,当然也可以使用枚举类名.实例名称获取 return INSTANCE; } }
    Processed: 0.009, SQL: 8