• 15、彻底玩转单例模式


    引用学习(狂神说)

    饿汉式 DCL懒汉式,深究!

    饿汉式创建单例

    饿汉式:顾名思义很饿:在类加载的时候,直接初始化对象

    • 缺点:很浪费资源,因为对象没有被使用,但是已经初始化在内存了

      • 比如:有下面这样的数组,会很浪费资源

    package com.zxh.single;
    
    /**
     *  饿汉式:顾名思义很饿
     *  1、在类加载的时候,直接初始化对象
     *  2、很浪费资源,因为对象没有被使用,但是已经初始化在内存了
     *      比如:有下面这样的数组,会很浪费资源
     */
    public class Hungry {
        private byte[] buffer1 = new byte[1024*1024];
        private byte[] buffer2 = new byte[1024*1024];
        private byte[] buffer3 = new byte[1024*1024];
        private byte[] buffer4 = new byte[1024*1024];
    
        // 构造器私有化
        private Hungry(){
    
        }
        // 直接初始化对象
        private final static Hungry HUNGRY = new Hungry();
    
        public static Hungry getInstance(){
            return HUNGRY;
        }
    
        public static void main(String[] args) {
            Hungry hungry1 = Hungry.getInstance();
            Hungry hungry2 = Hungry.getInstance();
            System.out.println(hungry1);
            System.out.println(hungry2);
        }
    }

     浪费资源所以就有了懒汉式创建单例模式

    懒汉式创建单例

    普通的懒汉式

    package com.zxh.single;
    
    /**
     * 懒汉式创建单例模式
     */
    public class LazyMan {
    
        // 构造器私有化
        private LazyMan(){
    
        }
    
        private static LazyMan LAZY_MAN;
    
        public static LazyMan getInstance(){
            if(LAZY_MAN == null){   // 如果为空,初始化对象
                LAZY_MAN = new LazyMan();
            }
            return LAZY_MAN;    // 并返回
        }
    
        public static void main(String[] args) {
            LazyMan lazyMan1 = LazyMan.getInstance();
            LazyMan lazyMan2 = LazyMan.getInstance();
            System.out.println(lazyMan1);
            System.out.println(lazyMan2);
        }
    
    }

    多线程破坏普通的懒汉式

    package com.zxh.single;
    
    /**
     * 懒汉式创建单例模式
     */
    public class LazyMan {
    
        // 构造器私有化
        private LazyMan(){
            System.out.println(Thread.currentThread().getName() + " OK");
        }
    
        private static LazyMan LAZY_MAN;
    
        public static LazyMan getInstance(){
            if(LAZY_MAN == null){   // 如果为空,初始化对象
                LAZY_MAN = new LazyMan();
            }
            return LAZY_MAN;    // 并返回
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    LazyMan.getInstance();
                }).start();
            }
        }
    
    }

    可以看到经过了4次构造方法,也就是创建了4个不同的对象。

    那么如何解决多线程的并发问题?使用DCL (双重检测锁)懒汉式

    DCL 懒汉式创建

    • DCL就是双重检测锁:解决并发问题

    增加同步代码块:解决并发问题

    • 修改getInstance这个方法

    public static LazyMan getInstance(){
        synchronized (LazyMan.class){   // 直接锁class模板
            if(LAZY_MAN == null){   // 如果为空,初始化对象
                LAZY_MAN = new LazyMan();
            }
        }
        return LAZY_MAN;    // 并返回
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }

    缺点:影响效率,因为每个线程都需要同步等待。

    解决:在外面增加判断如果对象已经创建,那么直接返回

    增加判断:解决效率问题

    • 解决同步的效率问题

    // DCL 双重检测锁
    public static LazyMan getInstance(){
        if (LAZY_MAN == null) { // 第一重检测
            synchronized (LazyMan.class){   // 直接锁class模板,锁
                if(LAZY_MAN == null){   // 如果为空,初始化对象,第二重检测
                    LAZY_MAN = new LazyMan();
                }
            }
        }
        return LAZY_MAN;    // 并返回
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }

    volatile:解决指令重排

    存在问题:指令重排

    // DCL 双重检测锁
    public static LazyMan getInstance(){
        if (LAZY_MAN == null) { // 第一重检测
            synchronized (LazyMan.class){   // 直接锁class模板,锁
                if(LAZY_MAN == null){   // 如果为空,初始化对象,第二重检测
                   /**
                     * 但是真的安全吗?不安全
                     * 因为初始化对象不是原子操作,在极端情况下会进行指令重排
                     * 初始化对象的时候,不要以为只有一行代码,但是执行的时候会分成3步
                     * 1、分配内存空间
                     * 2、执行构造方法,初始化对象
                     * 3、把这个对象指向这个空间
                     *
                     * 比如:我们希望执行的顺序为 123,
                     * 假如A线程进入,经过指令重排执行顺序为 132,当执行到13,还没有执行2的时候,内存空间是分配了也指向了这个空间,但是对象是空的
                     * 此时B线程进入了,发现对象已经分配了空间,直接返回了,就会造成空指针
                     */
                    LAZY_MAN = new LazyMan();
                }
            }
        }
        return LAZY_MAN;    // 并返回
    }

    解决问题

    • 增加volatile关键字,防止初始化对象的时候,计算机指令重排

    • private volatile static LazyMan LAZY_MAN;

    /**
     * 懒汉式创建单例模式
     */
    public class LazyMan {
    
        // 构造器私有化
        private LazyMan(){
            System.out.println(Thread.currentThread().getName() + " OK");
        }
    
        private volatile static LazyMan LAZY_MAN;
    
        // DCL 双重检测锁
        public static LazyMan getInstance(){
            if (LAZY_MAN == null) { // 第一重检测
                synchronized (LazyMan.class){   // 直接锁class模板,锁
                    if(LAZY_MAN == null){   // 如果为空,初始化对象,第二重检测
                        LAZY_MAN = new LazyMan();
                    }
                }
            }
            return LAZY_MAN;    // 并返回
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    LazyMan.getInstance();
                }).start();
            }
        }
    
    }

    静态内部类创建

    package com.zxh.single;
    
    // 静态内部类创建
    public class Holder {
    
        private Holder(){
    
        }
    
        public static InnerClass getInstance(){
            return InnerClass.INNER_CLASS;
        }
    
        // 静态内部类,在程序加载时,并不会被初始化,所以不会浪费资源
        private static class InnerClass{
            private final static InnerClass INNER_CLASS = new InnerClass();
        }
    
        public static void main(String[] args) {
            InnerClass innerClass1 = Holder.getInstance();
            InnerClass innerClass2 = Holder.getInstance();
    
            System.out.println(innerClass1);
            System.out.println(innerClass2);
        }
    
    }

    反射破坏DCL和防止破坏

    • 现在DCL 是目前我们认为最厉害的

    • 但是在反射面前,一切都时候浮云

    反射创建对象

    1、创建两个对象:第一个为普通创建,第二个使用反射创建

    • 会创建两个不同的对象

    package com.zxh.single;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    
    /**
     * 懒汉式创建单例模式
     */
    public class LazyMan {
    
        // 构造器私有化
        private LazyMan(){
        }
    
        private volatile static LazyMan LAZY_MAN;
    
        // DCL 双重检测锁
        public static LazyMan getInstance(){
            if (LAZY_MAN == null) { // 第一重检测
                synchronized (LazyMan.class){   // 直接锁class模板,锁
                    if(LAZY_MAN == null){   // 如果为空,初始化对象,第二重检测
    
                        LAZY_MAN = new LazyMan();
                    }
                }
            }
            return LAZY_MAN;    // 并返回
        }
    
        // NoSuchMethodException:没有这个方法
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            LazyMan lazyMan1 = LazyMan.getInstance();
            Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
            LazyMan lazyMan2 = lazyManConstructor.newInstance(null);
    
            System.out.println(lazyMan1);
            System.out.println(lazyMan2);
        }
    
    }

     解决:在构造器中在增加一重判断

    // 构造器私有化
    private LazyMan(){
        synchronized (LazyMan.class){
            if(LAZY_MAN != null)
                throw new RuntimeException("不要试图利用反射破坏单例");
        }
    }

     2、创建两个对象:两个对象都是用反射创建

    // NoSuchMethodException:没有这个方法
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
        LazyMan lazyMan1 = lazyManConstructor.newInstance(null);
        LazyMan lazyMan2 = lazyManConstructor.newInstance(null);
    
        System.out.println(lazyMan1);
        System.out.println(lazyMan2);
    }

     解决:增加一个标志变量,来判断是否是第一次创建对象

    • private static boolean flag = false:注意必须使用static修饰,才能是全局的变量

    package com.zxh.single;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    
    /**
     * 懒汉式创建单例模式
     */
    public class LazyMan {
    
        private static boolean flag = false;    // 增加一个标志,来判别是否已经创建了对象
    
        // 构造器私有化
        private LazyMan(){
            synchronized (LazyMan.class){
                if(flag == false){  // 第一次进入
                    flag = true;   // 表示已将创建了对象
                }else{
                    throw new RuntimeException("不要试图利用反射破坏单例");
                }
            }
        }
    
        private volatile static LazyMan LAZY_MAN;
    
        // DCL 双重检测锁
        public static LazyMan getInstance(){
            if (LAZY_MAN == null) { // 第一重检测
                synchronized (LazyMan.class){   // 直接锁class模板,锁
                    if(LAZY_MAN == null){   // 如果为空,初始化对象,第二重检测
                        LAZY_MAN = new LazyMan();
                    }
                }
            }
            return LAZY_MAN;    // 并返回
        }
    
        // NoSuchMethodException:没有这个方法
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
              LazyMan lazyMan1 = lazyManConstructor.newInstance(null);
            LazyMan lazyMan2 = lazyManConstructor.newInstance(null);
    
            System.out.println(lazyMan1);
            System.out.println(lazyMan2);
        }
    
    }

     3、通过修改字段属性来破坏

    // NoSuchMethodException:没有这个方法
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, ClassNotFoundException {
        Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
        LazyMan lazyMan1 = lazyManConstructor.newInstance(null);
        Field flag = LazyMan.class.getDeclaredField("flag");
        flag.setAccessible(true);   // 关闭安全监测锁,提高创建对象的效率
        flag.set(lazyMan1, false);
        LazyMan lazyMan2 = lazyManConstructor.newInstance(null);
    
        System.out.println(lazyMan1);
        System.out.println(lazyMan2);
    }

    哪怕这个标志的字段是加密的,也有可能会被反编译破解,从而获取字段信息。

    所以说魔高一尺,道高一丈!

     

    但是我们还可以利用枚举创建,因为它的底层就是不允许通过反射创建对象的

    枚举创建单例和分析

    枚举的单例创建

    package com.zxh.single;
    
    public enum EnumSingle {
        INSTANCE;
    
        public EnumSingle getInstance(){
            return INSTANCE;
        }
    
        public static void main(String[] args) {
            EnumSingle instance1 = EnumSingle.INSTANCE;
            EnumSingle instance2 = EnumSingle.INSTANCE;
            System.out.println(instance1 == instance2);
        }
    
    }

    源码分析反射的newInstance()方法

    1、进入newInstance()方法

    2、发现如果是枚举类,就抛出不能使用反射创建枚举类异常

    利用反射破坏测试

    package com.zxh.single;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    
    public enum EnumSingle {
        INSTANCE;
    
        public EnumSingle getInstance(){
            return INSTANCE;
        }
    
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            EnumSingle instance1 = EnumSingle.INSTANCE;
            Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(null);
            EnumSingle instance2 = constructor.newInstance(null);
    
            System.out.println(instance1 == instance2);
        }
    
    }

    反射调用时,存在问题并解决问题

    存在问题

    发现抛出异常时没有这个方法,和想象中会抛出的异常不同,为什么?

    是没有这个构造器吗?

    1、通过编译生成的target包中对应的class文件查看

    • 发现有这个构造器

    2、通过反编译class文件查看,进入指定的目录在命令行输入:javap -p EnumSingle.class

    • 发现也有这个构造器

     难道是idea和jdk骗了我们?

    3、使用专业的软件反编译

    jad百度网盘下载 提取码: 9fpa

    1)进入指定目录输入,jad -sjava EnumSingle.class

    • 需要将该执行文件和class文件放在一起,并且会生成在同一目录下

     

     2)编译得到的文件中可以发现没有空构造,但是有一个有参构造

     解决问题

    • 利用反射调用这个构造器
    package com.zxh.single;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    
    public enum EnumSingle {
        INSTANCE;
    
        public EnumSingle getInstance(){
            return INSTANCE;
        }
    
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            EnumSingle instance1 = EnumSingle.INSTANCE;
            Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
            EnumSingle instance2 = constructor.newInstance(null);
    
            System.out.println(instance1 == instance2);
        }
    
    }

    成功返回对应的异常

    总结

    枚举类是创建单例模式最安全的,推荐使用!

    致力于记录学习过程中的笔记,希望大家有所帮助(*^▽^*)!
  • 相关阅读:
    深入浅出Mybatis系列(一)Mybatis入门
    LinkedList其实就那么一回事儿之源码分析
    深入浅出Mybatis系列(八)mapper映射文件配置之select、resultMap
    ArrayList其实就那么一回事儿之源码浅析
    springMVC 源码解读系列(一)初始化
    深入浅出Mybatis系列(三)配置详解之properties与environments(mybatis源码篇)
    深入浅出Mybatis系列(四)配置详解之typeAliases别名(mybatis源码篇)
    深入浅出Mybatis系列(六)objectFactory、plugins、mappers简介与配置
    深入浅出Mybatis系列(二)配置简介(mybatis源码篇)
    深入浅出Mybatis系列(七)mapper映射文件配置之insert、update、delete
  • 原文地址:https://www.cnblogs.com/zxhbk/p/13028223.html
Copyright © 2020-2023  润新知