反射与单例模式笔记

    技术2024-07-11  69

    反射与单例模式笔记

    学自B站,遇见狂神

    一、恶汉式单例模式

    缺点:可能会浪费空间,开辟了空间,却没有使用 Hungry.class

    package com.wu; public class Hungry { //可能会浪费空间,开辟了空间,却没有使用 byte[] date1= new byte[1024*1024]; byte[] date2= new byte[1024*1024]; byte[] date3= new byte[1024*1024]; //第一步,私有化构造器 private Hungry(){ } //得到该类的一个实例 private static final Hungry hungry = new Hungry(); //提供得到该类实例的接口 public static Hungry getInstance(){ return hungry; } }

    二、懒汉式

    缺点:在多线程并发模式下容易出现并发问题。 在懒汉式基础上改进的DCL(双重检测+锁+原子操作)懒汉式解决并发问题。

    1. 懒汉式

    package com.wu; public class LzayMan { private LzayMan(){ System.out.println(Thread.currentThread().getName()+"OK"); } private static LzayMan lzayMan ; public static LzayMan getInstance(){ if(lzayMan == null){ lzayMan=new LzayMan(); //不是原子性操作,要么全部完成 /* * 1.分配内存空间 * 2、执行构造方法,初始化对象 * 3、把这个对象指向者个空间 * * 123 * 132 A线程执行顺序 * * B线程 //此时lazyMan还没有完成构造,因此lazyMan=null,再次构造单例 * */ } return lzayMan; } //测试,多并发条件下存在问题 public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{LzayMan.getInstance(); }).start(); } } }

    问题截图(每次运行的出现的线程数还不一样)

    2. DCL懒汉式(双重检测+锁+原子操作)

    注意:synchronized 解决并发问题,lzayMan=new LzayMan();//不是原子操作(跳出if执行return lzayMan,出现了分割操作),可能发生指令重排序的问题,通过volatil来解决。

    volatile 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语 义 (1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。 (2) 禁止进行指令重排序 volatile 不是原子性操作,volatitle保证部分有序性,当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行; x = 2; // 语 句 1 y = 0; // 语 句 2 flag = true; //语句3 x = 4; // 语 句 4 y = -1; // 语 句 5

    假设flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是 不作任何保证的。

    原子性: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作 一个很经典的例子就是银行账户转账问题:比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。 package com.wu.DCL; public class LzayMan { private LzayMan(){ System.out.println(Thread.currentThread().getName()+"OK"); } private volatile static LzayMan lzayMan ;//volatile解决指令重排序 //双重检测锁模式的 懒汉式单例 DCL懒汉式 public static LzayMan getInstance(){ if(lzayMan == null){ synchronized (LzayMan.class )//锁定一个类,线程串行执行 { if(lzayMan == null){ lzayMan=new LzayMan();//不是一个原子性操作 } } /* * 1.分配内存空间 * 2、执行构造方法,初始化对象 * 3、把这个对象指向者个空间 * * 123 * 132 A线程执行顺序 * * B线程 //此时lazyMan还没有完成构造,因此lazyMan=null,跳出if条件 * * */ } return lzayMan; } //测试 public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ LzayMan.getInstance(); }).start(); } } }

    三、静态内部类

    内部类单例,虚拟机加载内部类调用getInstance()产生的实例,能够保证线程安全,但是反射技术可以通过获取无参破坏单例:

    package com.wu; import java.lang.reflect.Constructor; public class Holder { private Holder(){ } public static Holder getInstance(){ return InnerClass.holder; } private static class InnerClass{ private static Holder holder = new Holder(); } public static void main(String[] args) throws Exception { //反射产生对象 Constructor<Holder> declaredConstructor = Holder.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true);//允许访问私有私有属性 Holder holder1 = Holder.getInstance(); Holder holder2 = declaredConstructor.newInstance(); System.out.println(holder1); System.out.println(holder2); } }

    四、DCL单例-反射破坏与保护

    1. 反射破坏单例与保护

    同理三,使用反射同样可以破坏DCL单例模式,通过对反射用到的构造器加入条件约束可以保护单例模式;但是,假如不用getInstance();得到实例都用反射获得,单例模式存在问题,解决措施是红绿灯思想,同样给构造器假加入约束条件利用反射破坏“红绿灯”标记位,单例模式同样存在问题,解决办法是后面的利用枚举来解决。 package com.wu.reflectDestorySigle; import java.lang.reflect.Constructor; import java.lang.reflect.Field; public class LazyMan1 { //反射要用到的构造器,设置条件约束阻止反射破坏 //第一个实验阻止反射破坏单例模式 /* private LazyMan1(){ synchronized (LazyMan.class){//通过加入锁,只有一个线程可以访问 if (lazyMan!=null){//说明lazyMan对象已经存在了 throw new RuntimeException("不要试图使用反射破坏单例"); } } System.out.println(Thread.currentThread().getName()+"OK"); } */ //第二个实验,红绿灯解决反射获得的实例单例模式存在问题 private static boolean qingjiang = false; private LazyMan1(){ synchronized (LazyMan1.class){//加锁,同一时刻只有一个线程可以访问 if (qingjiang==false){ qingjiang = true; }else{ throw new RuntimeException("不要试图使用反射破坏单例"); } } System.out.println(Thread.currentThread().getName()+"OK"); } private volatile static LazyMan1 lazyMan;//volatile禁止指令重排 //双重检测锁模式的 懒汉式单例 DCL懒汉式 public static LazyMan1 getInstance(){ if (lazyMan==null){ synchronized (LazyMan1.class){ if(lazyMan==null){ lazyMan = new LazyMan1();//不是一个原子性操作 } } } return lazyMan; } //反射! public static void main(String[] args) throws Exception { Field qingjiang = LazyMan1.class.getDeclaredField("qingjiang"); qingjiang.setAccessible(true); //LazyMan1 instance1 = LazyMan1.getInstance(); // 通过反射可以破坏单例模式 // 走了一个构造器,通过构造器中加入一个锁,不让它反射去破坏 Constructor<LazyMan1> declaredConstructor = LazyMan1.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true);//无视私有的构造器 LazyMan1 instance1 = declaredConstructor.newInstance(); qingjiang.set(instance1,false); LazyMan1 instance2 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } }

    2.基于枚举单例模式与反射

    public enum EnumSingle { INSTANCE; public EnumSingle getInstance() { return INSTANCE; } }

    使用反射来测试枚举

    class Test{ public static void main(String[] args) throws Exception { EnumSingle instance1 = EnumSingle.INSTANCE; Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); EnumSingle instance2 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } }

    但这并不是我们想看到的结果!!!

    com.wu.EnumSingle.<init>() at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.getDeclaredConstructor(Class.java:2178) at com.wu.Test.main(EnumSingle.java:16)

    枚举: 1、普通的反编译会欺骗开发者,说enum枚举是无参构造 2、实际enum为有参构造(见后面); 3、通过反射破解枚举会发现抛出异常(想要的结果) :Exception in thread “main” java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:417) at com.wu.Test.main(EnumSingle.java:17)

    通过jad工具反编译EnumSingle.class查看具体原因

    javap -p EnumSigle.class

    结果如下: 但事实上这个JDK和IDEA自带的jad也欺骗了我们,通过自备的开发工具jad放置与目标类文件一个目录下,双击jad.exe输入命令行(见下面)反编译枚举

    jad -sjava EnumSingle.class

    反编译枚举EnumSingle.java的代码如下:发现这个构造器其实是有参的,因此可以修改构造器的参素,尝试反编译枚举。 所以通过反射尝试破坏单例的正确程序为:

    import java.lang.reflect.Constructor; public enum EnumSingle { INSTANCE; public EnumSingle getInstance(){ return INSTANCE; } } class Test{ public static void main(String[] args) throws Exception { EnumSingle instance = EnumSingle.INSTANCE; //就只是修改了构造器这儿里 Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class); declaredConstructor.setAccessible(true); EnumSingle instance2 = declaredConstructor.newInstance(); //java.lang.NoSuchMethodException: com.ph.single.EnumSingle.<init>() System.out.println(instance); System.out.println(instance2); } }

    3.以上第四部分主要进行的工作:

    反射 破坏 单例模式阻止 反射破坏单例模式(构造器加锁,判断抛异常)两个对象都是反射出来的,破坏单例模式阻止反射出来的对象(红绿灯)反射破解红绿灯,破坏单例模式enum枚举保护单例模式通过jarde工具反编译拿到该类里面的构造器参数,设置参数尝试破坏单例,结果证明enum枚举类型保护单例模式不被反射破坏。 [参考文章]:(https://blog.csdn.net/pan_h1995/article/details/106517685)
    Processed: 0.030, SQL: 9