• 单例模式深层剖析


    单例模式

    饿汉模式

    public class Hungry {
        private Hungry(){
            
        }
        private final static Hungry hungry=new Hungry();
        public static Hungry getInstance(){
            return hungry;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    缺点:浪费存储空间

    内部类单例

    public class Holder {
        private Holder(){
            
        }
        
        public static Holder getInstance(){
            return InnerClass.holder;
        }
        
        public static class InnerClass{
            private static Holder holder=new Holder();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    此方法了解一下,即可,也是会浪费空间的

    懒汉模式

    懒汉模式的代码我是按照逐步升级维护的方式来演示的

    top1

    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

    这种方式在单线程模式下可以凑合着用,但是到了多线程的环境下,就会被破坏了

    以下代码是我简单的复现多线程来测试top1模式下的懒汉模式的缺陷

        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    LazyMan instance = LazyMan.getInstance();
                }).start();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    top2

    既然多线程会破坏第一步写的懒汉下的单例模式,我们可以进行加锁操作

        private LazyMan(){
            System.out.println(Thread.currentThread().getName());
        }
        private 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

    这次你可以自己复现以下,不论运行多少次都不会出现上面那种情况了。因为我们加了锁,并且做了两次判断lazyMan是否为空的操作

    那么新的问题又来了,虽然在这种情况下看似趋于完美了,但是你有没有考虑过反射会破环这个安全性呢?

    top3

    除了加锁,其实还个小细节,我们想一下,哪还有问题呢?

    lazyMan=new LazyMan();
    
    • 1

    这一步存在安全隐患!

    我们知道new相对内存级别来说不是原子操作

    new 的完整构建过程可以分为三步

    • 分配内存空间
    • 执行构造器,初始化对象
    • 将初始化的对象指向我们分配的内存空间

    这三部在多线程模式下,可不一定是按照 1,2,3,的顺序执行的,有可能线程1执行的步骤是1 3 2 那么线程1执行到3步骤的时候,(不加锁情况下)这时候线程2进来, 发现初始化对象已经指向了内存空间于是就返回了,但其实还没有初始化对象!

    为了解决原子性问题带来的安全隐患,我们可以使用

     private volatile static LazyMan lazyMan;
    
    • 1

    .volatile

     1.被设计用来修饰被不同的线程访问和修改的变量
     2、被volatile修饰的变量,系统每次用到它的时候,都是直接从对应的内存中取的,而不会利用缓存。这样就解决了多线程访问同一个变量的时候,所产生的不一致性
     3、被volatile修饰的变量,所有线程在任何时候所看到的变量的值都是相同的
    
    • 1
    • 2
    • 3

    top4

        public static void main(String[] args) throws Exception {
            LazyMan lazyMan1 = LazyMan.getInstance();
    
            Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            LazyMan lazyMan2 = constructor.newInstance();
    
            System.out.println(lazyMan1);
            System.out.println(lazyMan2);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    看一下上面我写的测试代码,这次我通过反射的方式来获取这个实例,打印的结果表名两者不是同一对象

    那么有没有解决办法呢?

    top5

    道高一尺,魔高一丈

    我们思考一下反射是如何创建实例的?

    反射的核心是先获取构造器,然后再通过构造器来获取实例。

    对应我们这个代码就是 反射获取我们的无参构造器,然后通过这个无参构造器来创建实例

    以子之矛攻子之盾 我们可以在无参构造器上加锁,如果创建LazyMan已经存在,抛异常

        private LazyMan(){
                synchronized (LazyMan.class){
                    if(lazyMan!=null) {
                        synchronized (LazyMan.class){
                            throw new RuntimeException("不要试图用反射来破环单例模式");
                        }
                    }
                }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    但是此场景下,还是会出问题,就是我们用反射来获取两个实例对象

    top5

        public static void main(String[] args) throws Exception {
    //        LazyMan lazyMan1 = LazyMan.getInstance();
    
            Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            LazyMan lazyMan1 = constructor.newInstance();
            LazyMan lazyMan2 = constructor.newInstance();
    
            System.out.println(lazyMan1);
            System.out.println(lazyMan2);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    此时连用两次反射创建实例还是会破环我们写的单例模式

    有一个方法可以解决此问题,前提是黑客或者客户不会通过反编译来获取我们定义的变量

    top6

        private static boolean flag=false;
        
        private LazyMan(){
            if (!flag){
                flag=true;
                synchronized (LazyMan.class){
                    if(lazyMan!=null) {
                        synchronized (LazyMan.class){
                            throw new RuntimeException("不要试图用反射来破环单例模式");
                        }
                    }
                }
            }else {
                throw new RuntimeException("不要试图用反射来破环单例模式");
            }
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我说过,前期是客户无法得知我们设置的 flag 变量(比如我们可以通过加密处理)

    但是还是有可能被黑客通过反射获取的这个变量的,那么我们的单例花式还是会被破坏

    top7

    public static void main(String[] args) throws Exception {
    
    //        LazyMan lazyMan1 = LazyMan.getInstance();
    
            Class<LazyMan> lazyManClass = LazyMan.class;
    
            Field flag = lazyManClass.getDeclaredField("flag");
            flag.setAccessible(true);
    
            Constructor<LazyMan> constructor = lazyManClass.getDeclaredConstructor(null);
            constructor.setAccessible(true);
    
            LazyMan lazyMan1 = constructor.newInstance();
    
            flag.set(lazyManClass,false);
    
            LazyMan lazyMan2 = constructor.newInstance();
    
            System.out.println(lazyMan1);
            System.out.println(lazyMan2);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    对此:我们就必须换一个方法了,枚举单例

    枚举单例

    我们查看反射当中 newInstance源码,可以看到

    Cannot reflectively create enum objects
    
    • 1

    源码当中就说了:不能试图通过反射来获取枚举对象

    public enum EnumSingle {
        INSTANCE;
    
        public EnumSingle getInstance() {
            return INSTANCE;
        }
    
        public static void main(String[] args) throws Exception {
            Constructor<EnumSingle> con = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
            con.setAccessible(true);
            EnumSingle enumSingle1 = con.newInstance();
            EnumSingle enumSingle2 = con.newInstance();
            System.out.println(enumSingle1);
            System.out.println(enumSingle2);
            //报错
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
  • 相关阅读:
    08/13摸鱼周
    阻性负载和感性负载的区别
    电脑重装系统后如何设置 win11 的默认登录方式
    最优性减枝
    2款Notepad++平替工具(实用、跨平台的文本编辑器)
    什么是mybatis,其实很简单
    Office Tool Plus下载与神龙版官网下载
    【UML】UML类图
    Flink状态
    典型行业大数据应用和安全风险和解决方案
  • 原文地址:https://blog.csdn.net/C_x_330/article/details/127438077