• 简单的单例模式其实也不简单


    单例模式可以说只要是一个合格的开发都会写,但是如果要深究,小小的单例模式可以牵扯到很多东西,比如 多线程是否安全,是否懒加载,性能等等。还有你知道几种单例模式的写法呢?如何防止反射破坏单例模式?今天,我就花一章内容来说说单例模式。

    关于单例模式的概念,在这里就不在阐述了,相信每个小伙伴都了如指掌。

    我们直接进入正题:

    饿汉式

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

    饿汉式是最简单的单例模式的写法,保证了线程的安全,在很长的时间里,我都是饿汉模式来完成单例的,因为够简单,后来才知道饿汉式会有一点小问题,看下面的代码:

    public class Hungry {
        private byte[] data1 = new byte[1024];
        private byte[] data2 = new byte[1024];
        private byte[] data3 = new byte[1024];
        private byte[] data4 = new byte[1024];
        
        private Hungry() {
        }
    
        private final static Hungry hungry = new Hungry();
    
        public static Hungry getInstance() {
            return hungry;
        }
    }
    
    

    在Hungry类中,我定义了四个byte数组,当代码一运行,这四个数组就被初始化,并且放入内存了,如果长时间没有用到getInstance方法,不需要Hungry类的对象,这不是一种浪费吗?我希望的是 只有用到了 getInstance方法,才会去初始化单例类,才会加载单例类中的数据。所以就有了 第二种单例模式:懒汉式。

    懒汉式(DCL)

    public class LazyMan {
        private LazyMan() {
        }
    
        private static LazyMan lazyMan;
    
        public static LazyMan getInstance() {
            if (lazyMan == null) {
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    }
    

    DCL懒汉式的单例,保证了线程的安全性,又符合了懒加载,只有在用到的时候,才会去初始化,调用效率也比较高,但是这种写法在极端情况还是可能会有一定的问题。因为

     lazyMan = new LazyMan();
    

    不是原子性操作,至少会经过三个步骤:

    1. 分配内存
    2. 执行构造方法
    3. 指向地址

    由于指令重排,导致A线程执行 lazyMan = new LazyMan();的时候,可能先执行了第三步(还没执行第二步),此时线程B又进来了,发现lazyMan已经不为空了,直接返回了lazyMan,并且后面使用了返回的lazyMan,由于线程A还没有执行第二步,导致此时lazyMan还不完整,可能会有一些意想不到的错误,所以就有了下面一种单例模式。

    懒汉式(Volatile)

    这种单例模式只是在上面DCL单例模式增加一个volatile关键字来避免指令重排:

    public class LazyMan {
        private LazyMan() {
        }
    
        private volatile static LazyMan lazyMan;
    
        public static LazyMan getInstance() {
            if (lazyMan == null) {
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    }
    

    持有者

    public class Holder {
        private Holder() {
        }
    
        public static Holder getInstance() {
            return InnerClass.holder;
        }
    
        private static class InnerClass {
            private static final Holder holder = new Holder();
        }
    }
    

    这种方式是第一种饿汉式的改进版本,同样也是在类中定义static变量的对象,并且直接初始化,不过是移到了静态内部类中,十分巧妙。既保证了线程的安全性,同时又满足了懒加载。

    万恶的反射

    万恶的反射登场了,反射是一个比较霸道的东西,无视private修饰的构造方法,可以直接在外面newInstance,破坏我们辛辛苦苦写的单例模式。

     public static void main(String[] args) {
            try {
                LazyMan lazyMan1 = LazyMan.getInstance();
                Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
                declaredConstructor.setAccessible(true);
                LazyMan lazyMan2 = declaredConstructor.newInstance();
                System.out.println(lazyMan1.hashCode());
                System.out.println(lazyMan2.hashCode());
                System.out.println(lazyMan1 == lazyMan2);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    我们分别打印出lazyMan1,lazyMan2的hashcode,lazyMan1是否相等lazyMan2,结果显而易见:

    image.png

    那么,怎么解决这种问题呢?

    public class LazyMan {
        private LazyMan() {
            synchronized (LazyMan.class) {
                if (lazyMan != null) {
                    throw new RuntimeException("不要试图用反射破坏单例模式");
                }
            }
        }
    
        private volatile static LazyMan lazyMan;
    
        public static LazyMan getInstance() {
            if (lazyMan == null) {
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    }
    

    在私有的构造函数中做一个判断,如果lazyMan不为空,说明lazyMan已经被创建过了,如果正常调用getInstance方法,是不会出现这种事情的,所以直接抛出异常:

    image.png

    但是这种写法还是有问题:

    上面我们是先正常的调用了getInstance方法,创建了LazyMan对象,所以第二次用反射创建对象,私有构造函数里面的判断起作用了,反射破坏单例模式失败。但是如果破坏者干脆不先调用getInstance方法,一上来就直接用反射创建对象,我们的判断就不生效了:

     public static void main(String[] args) {
            try {
                Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
                declaredConstructor.setAccessible(true);
                LazyMan lazyMan1 = declaredConstructor.newInstance();
                LazyMan lazyMan2 = declaredConstructor.newInstance();
                System.out.println(lazyMan1.hashCode());
                System.out.println(lazyMan2.hashCode());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    那么如何防止这种反射破坏呢?

    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 lazyMan;
        public static LazyMan getInstance() {
            if (lazyMan == null) {
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    }
    

    在这里,我定义了一个boolean变量flag,初始值是false,私有构造函数里面做了一个判断,如果flag=false,就把flag改为true,但是如果flag等于true,就说明有问题了,因为正常的调用是不会第二次跑到私有构造方法的,所以抛出异常:

    image.png

    看起来很美好,但是还是不能完全防止反射破坏单例模式,因为可以利用反射修改flag的值。

    看起来并没有一个很好的方案去避免反射破坏单例模式,所以轮到我们的枚举登场了。

    枚举

    public enum EnumSingleton {
        instance;
    }
    

    枚举是目前最推荐的单例模式的写法,因为足够简单,不需要开发自己保证线程的安全,同时又可以有效的防止反射破坏我们的单例模式,我们可以看下newInstance的源码:

    image.png
    重点就是红框中圈出来的部分,如果枚举去newInstance就直接抛出异常了。

    好了,这章的内容就结束了,下次再有人问你单例模式,再也不用害怕了。

  • 相关阅读:
    Spring源码阅读BeanFactory体系结构分析 coder
    Spring源码阅读IoC容器解析 coder
    Spring源码阅读ApplicationContext体系结构分析 coder
    【学习笔记】卷积神经网络 coder
    Spring源码阅读环境搭建 coder
    【学习笔记】分布式Tensorflow coder
    【spring实战第五版遇到的坑】3.1中的例子报错 coder
    阿里云服务器磁盘空间不足解决办法
    Tomcat配置https SSL证书
    mybatis:Creating a new SqlSession Closing non transactional SqlSession
  • 原文地址:https://www.cnblogs.com/CodeBear/p/10212529.html
Copyright © 2020-2023  润新知