zoukankan      html  css  js  c++  java
  • 单例模式笔记

    image-20200531213054908

    学习资源来自于哔哩哔哩UP遇见狂神说,一个宝藏UP大家快去关注吧!记得三连加分享,不要做白嫖党.

    单例模式

    饿汉式单例

    public class Hungry {
        private Hungry() {
    
        }
    
        private static final Hungry HUNGRY= new Hungry();
    
        public static Hungry getInstance() {
            return HUNGRY;
        }
    
    }
    

    **单例模式中最重要的思想: **构造器私有,一旦构造器私有被人就没有办法new这个对象,保证内存中只有一个对象.

    饿汉式一上来就new出来这个对象,这样就可以保证它是唯一的.再抛出一个对外的方法.

    饿汉式的问题: 一上来就会把所有东西加载出来,非常耗内存资源,可能会浪费空间

    解决方法: 想要用的时候再去创造这个对象,平时就放着,于是就出来了懒汉式单例

    懒汉式单例

    //懒汉式单例
    public class LazyMan {
    
        private LazyMan() {
            System.out.println(Thread.currentThread().getName() + "ok");
        }
    
        private static LazyMan lazyMan;
    
        public static LazyMan getInstance() {
            if (lazyMan == null) {
                lazyMan = new LazyMan();
            }
            return lazyMan;
        }
    }
    

    当对象为空时才创建对象.

    问题: 单线程下确实OK,但是多线程并发下有问题

    写一个多线程的main方法:

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                lazyMan.getInstance();
            }).start();
        }
    }
    
    输出:
    Thread-8ok
    Thread-7ok
    Thread-5ok
    Thread-2ok
    Thread-3ok
    Thread-1ok
    Thread-4ok
    Thread-9ok
    Thread-0ok(每次结果都不一样)
    

    多线程下有问题无法单例,

    那么我们就得加一把锁,但我们得考虑一种情况:
    两个线程同时到达,即同时调用 getInstance() 方法,此时由于 lazyMan == null ,所以很明显,两个线程都可以通过第一重的 lazyMan == null ,进入第一重 if语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 lazyMan == null ,而另外的一个线程则会在 lock 语句的外面等待.

    所以需要双重锁检测null.(虽然我单个锁检测了好久都没出问题)

    public class LazyMan {
    
        private LazyMan() {
            System.out.println(Thread.currentThread().getName() + "ok");
        }
    
        private static LazyMan lazyMan;
    
        public static LazyMan getInstance() {
            //双重检测锁模式的懒汉式单例 DCL懒汉式
            if (lazyMan == null) {
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    }
    
    输出:
    Thread-0ok
    

    虽然加了双重锁,但是在极端环境下仍然不安全.

    因为lazyMan = new LazyMan();不是一个原子性操作,

    三个指令:

    1. 分配内存空间
    2. 执行构造方法,初始化对象
    3. 把这个对象指向这个空间

    正常按1,2,3顺序执行不会出问题,但如果是按照1,3,2顺序执行:

    先占用空间,再指向对象.

    如果只有一个线程A按1,3,2执行,此时来了一个线程B,由于A已经指向了这个空间它会认为lazyMan != null,就不会实例化,直接return, B没有完成构造.

    所以需要加一个volatile保证原子性.

    image-20200529225522920

    添加一个volatile,避免指令重排.

    image-20200529225551199

    静态内部类单例

    public class Holder {
        private Holder() {
    
        }
    
        //获取实例
        public static Holder getInstance() {
            return InnerClass.HOLDER;
        }
        
        //在静态内部类中实例化对象
        public static class InnerClass {
            private static final Holder HOLDER = new Holder();
        }
    }
    

    没什么实际意义....


    这三个单例尽管又是构造器私有,又是加锁的,

    但是在仍然不安全,因为有反射的存在.

    反射破解单例模式!

    怎么算破解了呢: 只要能一次性new出两个实例就算是破解了单例.

    反射破解懒汉式并new一个新对象

    破解最难破解的懒汉式,核心破解代码:

    //反射得到私有的空参构造器
    Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
    //关闭检测,无视私有构造器,可以通过反射构造对象
    declaredConstructor.setAccessible(true);
    //通过反射new出来一个实例instance2
    LazyMan instance2 = (LazyMan) declaredConstructor.newInstance();
    

    完整代码:

    public class LazyMan {
    
        private LazyMan() {
    
        }
    
        private volatile static LazyMan lazyMan;
    
        public static LazyMan getInstance() {
            //双重检测锁模式的懒汉式单例 DCL懒汉式
            if (lazyMan == null) {
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    
        public static void main(String[] args) throws Exception {
            //懒汉式自身创建实例对象instance
            LazyMan instance = LazyMan.getInstance();
    
            //反射得到私有的空参构造器
            Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
            //关闭检测,无视私有构造器,可以通过反射构造对象
            declaredConstructor.setAccessible(true);
            //通过反射new出来一个实例instance2
            LazyMan instance2 = (LazyMan) declaredConstructor.newInstance();
    
            //如果破坏了两个实例就不一样
            System.out.println(instance.hashCode());
            System.out.println(instance2.hashCode());
        }
    
    }
    
    输出:
    284720968
    189568618
    

    hashCode不同,代表我们通过反射无视了懒汉式中的私有构造器,并new了一个新对象.

    懒汉式防反射破解

    因为反射走了无参构造器,所以我们可以在构造器中加一把锁.

    空参构造器加锁后:

    private LazyMan() {
        synchronized (LazyMan.class){
            //如果在单例中已经new了lazyMan,那么用反射new对象的时候就会出错.
            if (lazyMan != null) {
                throw new RuntimeException("不要试图用反射破解");
            }
        }
    }
    

    防破解原理: 如果在单例中已经new了lazyMan,那么用反射new的时候就会出错.

    运行结果:

    image-20200529233159400

    反射破解三锁懒汉式

    因为在之前的反射代码中有用懒汉式自身new一个对象,

    //懒汉式自身创建实例对象instance
    LazyMan instance = LazyMan.getInstance();
    

    导致触发了私有构造器中的锁.那么都用反射创建就好了,不走私有构造器不就不会触发锁了.

    public static void main(String[] args) throws Exception {
        //        //反射获取原来的实例instance
        //        LazyMan instance = LazyMan.getInstance();
    
        //反射得到私有的空参构造器
        Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        //关闭检测,无视私有构造器,可以通过反射构造对象
        declaredConstructor.setAccessible(true);
        //通过反射new出来实例
        LazyMan instance = (LazyMan) declaredConstructor.newInstance();
        LazyMan instance2 = (LazyMan) declaredConstructor.newInstance();
    
        //如果破坏了两个实例就不一样
        System.out.println(instance.hashCode());
        System.out.println(instance2.hashCode());
    }
    
    输出:
    284720968
    189568618
    

    添加标志位防破解

    设置一个标志位,在任何情况下,new对象都会使得标志位改变.那么第二次new对象的时候flag==true,就会触发异常.保证一次只能new一个对象.(标志位可以加密,得不到标志位就没办法继续用反射破解单例)

    添加一个标指位之后:

    private static boolean flag = false;
    
    private LazyMan() {
        synchronized (LazyMan.class){
            //不论通不通过反射flag总会变
            if (!flag) {
                flag = true;
            }else {
                throw new RuntimeException("不要试图用反射破解");
            }
        }
    }
    

    输出:

    image-20200530003451644

    继续破解

    再厉害的加密也会被破解,假设找了标志位,就可以关闭标志位的检测并重新置为false.

    增加的代码:

    //得到标志位并关闭标志位的检测
    Field flag = LazyMan.class.getDeclaredField("flag");
    flag.setAccessible(true);
    
    //把标志位再置为false
    flag.set(instance,false);
    

    源代码:

    public static void main(String[] args) throws Exception {
    
        //得到标志位并关闭标志位的检测
        Field flag = LazyMan.class.getDeclaredField("flag");
        flag.setAccessible(true);
    
        //反射得到私有的空参构造器
        Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        //关闭检测,无视私有构造器,可以通过反射构造对象
        declaredConstructor.setAccessible(true);
        //通过反射new出来实例
        LazyMan instance = (LazyMan) declaredConstructor.newInstance();
    
        //把标志位再置为false
        flag.set(instance,false);
    
        LazyMan instance2 = (LazyMan) declaredConstructor.newInstance();
    
        //如果破坏了两个实例就不一样
        System.out.println(instance.hashCode());
        System.out.println(instance2.hashCode());
    }
    
    输出:
    1313922862
    495053715
    

    到这里似乎没辙了,毕竟反射可以获得和修改类的所有信息.那么怎么办呢?

    分析反射原码来防反射

    image-20200530005105106

    点进反射的这个创建对象的方法

    image-20200530005235693

    在反射创建对象的方法的原码中我们可以发现这样一句话: 不要用反射来创建枚举对象

    也就是说反射从原码上就无法破坏枚举,那么我们测试一下.

    测试用反射破坏枚举

    先写个简单的枚举:

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

    由于枚举是自带单例的,所以里面肯定有一个私有的构造方法,于是在idea中打开class文件文件,查看私有的构造方法.

    package com.tan.single;
    
    public enum EnumSingle {
        INSTANCE;
    
        //这里空参构造
        private EnumSingle() {
        }
    
        public EnumSingle getInstance() {
            return INSTANCE;
        }
    }
    

    可以发现在枚举中这是一个私有空参构造方法

    那么我们来试试用反射破解一下

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

    先用枚举自身获取一个对象,再用反射获取一个对象,对比一下两个对象是否一样.

    image-20200530011400890

    报错,意料之中.

    但是报的错误并不是反射源代码中的Cannot reflectively create enum objects,而是com.tan.single.EnumSingle.<init>()这就很奇怪了

    按报错来看,我们需要声明并初始化,那么我们随便加一条语句初始化一下.

    private final Map<String, String> props = new ConcurrentHashMap<String, String>();
    

    看看class文件

    package com.tan.single;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    public enum EnumSingle {
        INSTANCE;
    
        private final Map<String, String> props = new ConcurrentHashMap();
    
        private EnumSingle() {
        }
    
        public EnumSingle getInstance() {
            return INSTANCE;
        }
    }
    

    发现空参构造仍然在,但是反射还是没办法利用这个空参构造破解,说明确实没办法用反射破解枚举.

    但是从原码上来看,用反射破解枚举应该会报错并抛出Cannot reflectively create enum objects,为什么报错会与理论都会不一样呢?

    探究报错为什么会与理论不一样

    这个编译后class文件一定有问题,idea的反编译不行那么我们试试用cmd来反编译

    image-20200530013435556

    发现仍然有空参构造,还是不行.

    试试专业反编译工具 jad.exe

    image-20200530013808082

    打开反编译后的java文件

    image-20200530013821379

    发现终于不是空参了

    image-20200530013918013

    改一下反射代码

    image-20200530014133162

    完整代码 EnumSingle.java

        package com.tan.single;
    
        import java.lang.reflect.Constructor;
        import java.util.Map;
        import java.util.concurrent.ConcurrentHashMap;
    
        /**
         * @author tan
         * @date 2020/5/30
         */
    
        public enum EnumSingle {
    
            INSTANCE;
            private final Map<String, String> props = new ConcurrentHashMap<String, String>();
    
            public EnumSingle getInstance() {
                return INSTANCE;
            }
        }
    
        class Test {
    
            public static void main(String[] args) throws Exception {
                EnumSingle enumSingle1 = EnumSingle.INSTANCE;
    
                Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
                declaredConstructor.setAccessible(true);
                EnumSingle enumSingle2 = declaredConstructor.newInstance();
    
                System.out.println(enumSingle1);
                System.out.println(enumSingle2);
            }
        }
    

    运行结果:

    image-20200530014350872

    好了,终于是报了正确的错误,探究至此结束.反射破解不了枚举,而枚举自带单例,用枚举写的单例模式自然也无法用反射破坏.

  • 相关阅读:
    CSS页面渲染优化属性will-change
    前端自动化构建工具-yoman浅谈
    【积累】如何优雅关闭SpringBoot Web服务进程
    SpringCloud Eureka Client和Server侧配置及Eureka高可用配置
    SpringBoot返回html页面
    MySQL8主从配置
    使用Arrays.asList抛出java.lang.UnsupportedOperationException
    SpringMVC+Mybatis+MySQL8遇到的问题
    MySQL5.6启用sha256_password插件
    Base64简单原理
  • 原文地址:https://www.cnblogs.com/tanshishi/p/13021522.html
Copyright © 2011-2022 走看看