• 你了解单例模式吗?


    彻底玩转单例模式

    之所以使用private声明一个私有的构造器,其主要目的就是为了不让用户创建新的实例对象

    ​ ——亚里士多德

    1. 前景知识

    进行实例化的时候的几个步骤

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

    (指令重排可能会打乱这三个顺序)

    😧什么是单例模式?

    保证类在内存中只能有一个对象,且构造私有的类设计模式,他主要解决了一个全局使用的类频繁地创建与销毁。

    🐕 单例模式的好处:

    • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
    • 避免对资源的多重占用(比如写文件操作)。

    😧缺点:

    • 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

    1.1 饿汉式单例模式

    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  Hungry  hungry= new Hungry();
    
        private static Hungry getHungry(){
            return hungry;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 缺陷,比如一个类在初始化的过程中需要初始化特别多的变量,占用很多空间,那么饿汉式的设计就会在还没有使用的时候就把空间分配了, 浪费空间

    1.2 懒汉式单例模式

    public class LazyMan {
    
        private LazyMan(){
            System.out.println(Thread.currentThread().getName() + "   " + "初始化了一个");
        }
        private static  LazyMan lazyMan;
    
        public static LazyMan getInstance(){
            if(lazyMan == null){
                lazyMan = new LazyMan();
            }
            return lazyMan;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这种情况看似没有问题,是因为在单线程情况下没u有问题,一旦在多线程场景下,问题立马就出来了,可能会实例化好多个LazyMan

    如开10个线程 ,会初始化5个,按理应该是1个:

    在这里插入图片描述

    👇

    我们可以想到的方法是,为其加锁 第一个想到的是sychronized

    //双重锁检测机制
        public static LazyMan getInstance(){
            if(lazyMan == null){
                synchronized (LazyMan.class){
                    if (lazyMan == null){
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    但是这样看似解决了多线程的问题,但是仍然还有其他问题,那就是更底层的指令重排问题。

    因为lazyMan = new LazyMan()本身就不是一个原子性操作,当你执行这条指令的时候,会经历以下步骤

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

    如果大家都是按照 1 2 3顺序来做,肯定是没问题的,但是synchronized只是锁对象,并不保证底层禁止指令重排,这就会有严重的事情,比如:

    两个线程A,B分别以 123 与 132的顺序执行的指令。 当B执行完3号指令的时候,由于某种原因卡住了,此时A观察到lazyMan已经不为null了,那么就会返回lazyMan. 但其实此时的lazyMan还没有被初始化

    👇

    说起禁止指令重排,我们立马想到了volatile,是的没错,只需要为我们的懒汉加上volatile关键字即可了 ==== 》(齐全之后的懒汉模式被称为DCL懒汉模式)

    private volatile static  LazyMan lazyMan;
    
    • 1

    DCL懒汉模式代码:

    package com.kai.single;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    
    public class LazyMan {
    
        private LazyMan(){
            System.out.println(Thread.currentThread().getName() + "   " + "初始化了一个");
        }
        private volatile static  LazyMan lazyMan;
    
        //双重锁检测机制
        public static LazyMan getInstance(){
            if(lazyMan == null){
                synchronized (LazyMan.class){
                    if (lazyMan == null){
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    2. 有没有什么其他方法破坏单例模式呢?

    答案是肯定的,因为java中存在反射机制,而反射机制会破坏掉很多看似很安全的机制。

    接着刚刚的lazyMan代码,我们继续写一个main方法,用反射机制来进行破坏。

    //反射
    public static void main(String[] args) throws Exception {
            LazyMan instaince = LazyMan.getInstance();
    				//拿到他的无参构造器
            Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            LazyMan instance2 = constructor.newInstance();
    				//输出两个实例,看是否是同一个实例
            System.out.println(instaince);
            System.out.println(instance2);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    事实证明,单例模式确实是被破坏掉了。

    👇

    你可能会说,那我在构造器里面再加锁不就行了

    private LazyMan() throws Exception {
            synchronized (LazyMan.class){
                if(lazyMan == null){
                    System.out.println(Thread.currentThread().getName() + "   " + "初始化了一个");
                }else{
                    throw  new Exception("禁止使用反射来破坏单例");
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    可能你觉得胜利了,但是道高一尺,魔高一丈

    👇

    此时,我们再来修改以下main方法。让两个实例都是通过反射来制造

    public static void main(String[] args) throws Exception {
            //LazyMan instaince = LazyMan.getInstance();
            Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
            constructor.setAccessible(true);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    此时又出现了刚刚的情况。

    👇

    你可能又会说,那我构造一个私有的变量来检测构造方法有没有被执行不就可以了?

    让我们看一下这样的具体做法

    private  static boolean judge = false;
    
        private LazyMan() throws Exception {
            synchronized (LazyMan.class){
                if(lazyMan == null){
                    if(judge == false) judge = true;
                    else throw new Exception("禁止使用反射机制破坏");
                    System.out.println(Thread.currentThread().getName() + "   " + "初始化了一个");
                }else{
                    throw  new Exception("禁止使用反射来破坏单例");
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    此时你可能又觉得没问题了,可惜道高一尺魔高一丈。

    👇

    让我们再次修改main方法中的代码吗

    public static void main(String[] args) throws Exception {
            //LazyMan instaince = LazyMan.getInstance();
            Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
            //通过反射获取属性
            val judge = LazyMan.class.getDeclaredField("judge");
            judge.setAccessible(true);
            constructor.setAccessible(true);
            LazyMan instance2 = constructor.newInstance();
    
            //把judge设置为false,就可以继续创建实例了
            judge.set(constructor,false);
    
            LazyMan instance = constructor.newInstance();
    
            System.out.println(instance);
            System.out.println(instance2);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    看到这里,你是不是觉得很累,反射好像是永远都有他的对策,,,,, 没错,这也正是懒汉模式单例的致命缺点。

    3. 关于枚举

    枚举是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化,多线程安全。

    它可以有效的防止反射。

    public enum  EnumSingleton {
        INSTANCE;
         public void outPut(){
            System.out.println("=================");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    他所对应的class文件如下

    package li;
    
    public enum EnumSingleton {
        INSTANCE;
    
        private EnumSingleton() {
        }
    
        public void outPut() {
            System.out.println("=================");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    尝试使用反射机制破坏

    class test5{
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(null);
            declaredConstructor.setAccessible(true);
            EnumSingleton enumSingleton = declaredConstructor.newInstance();
            enumSingleton.outPut();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注意: 我们这里仍然是通过null来调用无参构造。 我们运行一下看效果

    Exception in thread "main" java.lang.NoSuchMethodException: li.EnumSingleton.<init>()
    	at java.lang.Class.getConstructor0(Class.java:3082)
    	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    	at li.test5.main(EnumSingleton.java:15)
    
    • 1
    • 2
    • 3
    • 4

    他竟然提示我们该类中没有无参构造。。。一定是有什么猫腻在其中,毕竟我们刚刚亲手写了无参构造

    👇

    尝试用jad进行反编译,先要打开jad,然后在cmd输入:jad -sjava EnumSingleton.class

    // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
    // Jad home page: http://www.kpdus.com/jad.html
    // Decompiler options: packimports(3) 
    // Source File Name:   EnumSingleton.java
    
    package li;
    
    import java.io.PrintStream;
    
    public final class EnumSingleton extends Enum
    {
    
        public static EnumSingleton[] values()
        {
            return (EnumSingleton[])$VALUES.clone();
        }
    
        public static EnumSingleton valueOf(String name)
        {
            return (EnumSingleton)Enum.valueOf(li/EnumSingleton, name);
        }
    	
    	//看这里,发现是有两个参数的有参构造方法
        private EnumSingleton(String s, int i)
        {
            super(s, i);
        }
    
        public void outPut()
        {
            System.out.println("\u7EA2\u706B\u706B\u604D\u604D\u60DA\u60DA");
        }
    
        public static final EnumSingleton INSTANCE;
        private static final EnumSingleton $VALUES[];
    
        static 
        {
            INSTANCE = new EnumSingleton("INSTANCE", 0);
            $VALUES = (new EnumSingleton[] {
                INSTANCE
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    可以看出来, 他确实是虚晃了我们一枪,其实他是有着带两个参数的构造器的,那我们就换一种获取构造器的方法。(用反射来尝试使用有参构造)

    class test5{
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            //使用有参构造
     Constructor<EnumSingleton> declaredConstructor = 	      EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
            declaredConstructor.setAccessible(true);
            
            EnumSingleton enumSingleton = declaredConstructor.newInstance();
            enumSingleton.outPut();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    	at li.test5.main(EnumSingleton.java:17)
    
    • 1
    • 2
    • 3

    结果告诉我们不能用反射来创建枚举对象。 这才是我们真正想要的异常

    总结一下

    一般情况下,不建议使用第 2 懒汉方式,建议使用第 1 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 4 种静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用第 5 种枚举方式。如果有其他特殊的需求,可以考虑使用第 3 种双检锁方式。

  • 相关阅读:
    Python3 面向对象
    [C++] 小游戏 斗破苍穹 2.2.1至2.11.5所有版本(中) zty出品
    后厂村路灯:【干货分享】AppleDevelop苹果开发者到期重置
    科创人·望繁信创始人索强:中国版流程挖掘注定有完全不同的活法
    【夯实算法基础】最近公共祖先
    Vue2转Vue3快速上手第一篇(共两篇)
    三十九、jQuery
    前端 TS 快速入门之六:枚举 enum
    ASP.NET Core 6框架揭秘实例演示[38]:两种不同的限流策略
    小米发布CyberOn仿生机器人;多伦多大学『3D和几何深度学习』课程资料;英伟达神经场工具库;商汤开源项目集锦;前沿论文 | ShowMeAI资讯日报
  • 原文地址:https://blog.csdn.net/qq_42392049/article/details/127429845