zoukankan      html  css  js  c++  java
  • 简单的单例模式其实也不简单

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

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

    我们直接进入正题:

    饿汉式

    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就直接抛出异常了。

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

  • 相关阅读:
    Android 按键消息处理Android 按键消息处理
    objcopy
    SQLite多线程读写实践及常见问题总结
    android动画坐标定义
    Android动画效果translate、scale、alpha、rotate
    Android公共库(缓存 下拉ListView 下载管理Pro 静默安装 root运行 Java公共类)
    Flatten Binary Tree to Linked List
    Distinct Subsequences
    Populating Next Right Pointers in Each Node II
    Populating Next Right Pointers in Each Node
  • 原文地址:https://www.cnblogs.com/CodeBear/p/10212529.html
Copyright © 2011-2022 走看看