zoukankan      html  css  js  c++  java
  • Java中单例模式的安全性分析

    文章首发于我的博客,欢迎访问:https://blog.itzhouq.cn/singleton

    单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

    本文主要介绍以下内容:

    1. 两种单例模式
    2. 单例模式的线程安全验证
    3. 双重检测锁模式的单例模式
    4. 反射破坏单例模式
    5. 反编译字节码的两种方式

    1、饿汉式单例模式

    最简单的饿汉式单例模式代码:

    package cn.itzhouq.single;
    
    /**
     * 饿汉式单例:
     *      1. 私有化空参构造器
     *      2. 私有状态直接创建对象
     *      3.提供公有的获得对象的方法
     */
    public class Hungry {
    
        // 可能会浪费空间
        private byte[] data1 = new byte[1024 * 2014];
        private byte[] data2 = new byte[1024 * 2014];
        private byte[] data3 = new byte[1024 * 2014];
        private byte[] data4 = new byte[1024 * 2014];
    
        private Hungry () {
    
        }
    
        private final static Hungry HUNGRY = new Hungry();
    
        public static Hungry getInstance() {
            return HUNGRY;
        }
    }
    

    为了防止资源浪费,我们需要在使用对象的时候再去创建单例对象,所以就有了懒汉式。

    2、懒汉式单例模式

    最简单的懒汉式单例:

    package cn.itzhouq.single;
    
    /**
     * 懒汉式单例
     */
    public class LazyMan {
    
        private LazyMan () {
    
        }
    
        private static LazyMan lazyMan;
    
        public static LazyMan getInstance () {
            if (lazyMan == null) {
                lazyMan = new LazyMan();
            }
            return lazyMan;
        }
    }
    

    上述代码在单线程是 OK 的,但是在多线程下是不安全的。现在在多线程下做个测试:

    package cn.itzhouq.single;
    
    /**
     * 懒汉式单例
     */
    public class LazyMan {
    
        private LazyMan () {
            System.out.println(Thread.currentThread().getName() + "ok");
        }
    
        private static LazyMan lazyMan;
    
        public static LazyMan getInstance () {
            if (lazyMan == null) {
                lazyMan = new LazyMan();
            }
            return lazyMan;
        }
    
        // 多线程并发
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread(() -> {
                    LazyMan.getInstance();
                }).start();
            }
        }
    }
    

    结果是创建的对象大概率不是单例:

    Thread-0ok
    Thread-3ok
    Thread-2ok
    Thread-1ok
    

    所以我们需要加锁解决这个问题。

    3、双重检测锁模式的单例模式(DCL 懒汉式)

    package cn.itzhouq.single;
    
    /**
     * 懒汉式单例
     */
    public class LazyMan {
    
        private LazyMan () {}
    
        private static LazyMan lazyMan;
    
        // 双重检测锁模式的懒汉式单例 DCL 懒汉式
        public static LazyMan getInstance () {
            if (lazyMan == null) {
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    }
    

    这种懒汉式在极端情况下还是有问题的,问题出现在lazyMan = new LazyMan();

    因为这一步不是原子性操作,会经历以下三步:

    分配内存空间
    执行构造方法,初始化对象
    把这个对象指向这个空间
    

    在底层可能会出现指令重排,比如我们预期这三步的顺序是 123, 但是真实可能出现 132。也就是线程 A 首先执行1,再执行3,这个在 CPU 中是可以做到的。

    A线程下首先分配了内存空间,然后这个对象指向某个空间,但是第2步没有执行,没有初始化对象。

    这个时候如果有一个B线程开始执行,执行到16行,判断 lazyMan 是否为 null。由于这个对象已经指向了特定空间,所以不为 null ,直接返回 lazyMan ,这个时候对象还没有执行构造方法,进行初始化。

    所以为了防止指令重排,必须加上 volatile。关于volatile,以后再更新相关内容,读者也可以自行百度,学习相关知识。

    package cn.itzhouq.single;
    
    /**
     * 懒汉式单例
     */
    public class LazyMan {
    
        private LazyMan () {
            System.out.println(Thread.currentThread().getName() + "ok");
        }
    
        // 添加 volatile 保证原子性
        private volatile static LazyMan lazyMan;
    
        // 双重检测锁模式的懒汉式单例 DCL 懒汉式
        public static LazyMan getInstance () {
            if (lazyMan == null) {
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan(); // 不是原子性操作
                    }
                }
            }
            return lazyMan;
        }
    }
    

    面试题的时候至少要说出以上几点。

    4、静态内部类实现单例模式(了解)

    还有一种方式可以实现单例模式 --- 内部类。

    package cn.itzhouq.single;
    
    /**
     * 静态内部类实现单例模式
     */
    public class Holder {
        private Holder() {
    
        }
    
        public static Holder getInstance() {
            return InnerClass.HOLDER;
        }
    
        public static class InnerClass {
            public static final Holder HOLDER = new Holder();
        }
    }
    

    这种方式了解一下。

    5、使用反射破坏单例

    上面的单例模式日常开发中够用,但要知道DCL 懒汉式代码也不是绝对安全的。使用反射就能破坏其安全性。

    // 反射
    public static void main(String[] args) throws Exception {
        LazyMan instance = lazyMan.getInstance();
        // 使用反射获取空参构造器
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        // 取消权限检查
        declaredConstructor.setAccessible(true);
        LazyMan instance2 = declaredConstructor.newInstance();
        System.out.println(instance == instance2); // false
    }
    

    获取到的两个对象不相等,说明不是单例模式。

    试图通过标志位解决这个问题:

    package cn.itzhouq.single;
    import java.lang.reflect.Constructor;
    /**
     * 懒汉式单例
     */
    public class LazyMan {
    
        private static boolean flag = false;
    
        private LazyMan () {
            synchronized (LazyMan.class) {
                if (!flag) {
                    flag = true;
                } else {
                    throw new RuntimeException("不要试图通过反射破坏单例。");
                }
            }
        }
    
        private volatile static LazyMan lazyMan;
    
        // 双重检测锁模式的懒汉式单例 DCL 懒汉式
        public static LazyMan getInstance () {
            if (lazyMan == null) {
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan(); // 不是原子性操作
                    }
                }
            }
            return lazyMan;
        }
    }
    
    // 反射
    public static void main(String[] args) throws Exception {
        // LazyMan instance = lazyMan.getInstance();
        // 使用反射获取空参构造器
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        // 取消权限检查
        declaredConstructor.setAccessible(true);
        LazyMan instance1 = declaredConstructor.newInstance();
        LazyMan instance2 = declaredConstructor.newInstance();
        System.out.println(instance1 == instance2); // 抛出异常
    }
    

    如果这里的 flag 外界看不到的,这样就可以解决了,但是如果这个私有的属性也被暴露了也还是不安全。

    下面的代码通过反射修改属性的值。

    package cn.itzhouq.single;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    
    /**
     * 懒汉式单例
     */
    public class LazyMan {
    
        private static boolean flag = false;
    
        private LazyMan () {
            synchronized (LazyMan.class) {
                if (!flag) {
                    flag = true;
                } else {
                    throw new RuntimeException("不要试图通过反射破坏单例。");
                }
            }
        }
    
        private volatile static LazyMan lazyMan;
    
        // 双重检测锁模式的懒汉式单例 DCL 懒汉式
        public static LazyMan getInstance () {
            if (lazyMan == null) {
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan(); // 不是原子性操作
                    }
                }
            }
            return lazyMan;
        }
    }
    
    // 反射
    public static void main(String[] args) throws Exception {
        // LazyMan instance = lazyMan.getInstance();
    
        Field flag = LazyMan.class.getDeclaredField("flag");
        flag.setAccessible(true);
    
        // 使用反射获取空参构造器
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        // 取消权限检查
        declaredConstructor.setAccessible(true);
        LazyMan instance1 = declaredConstructor.newInstance();
    
        flag.set(instance1, false);
        LazyMan instance2 = declaredConstructor.newInstance();
        System.out.println(instance1 == instance2); // false
    }
    

    6、枚举实现单例模式

    查看newInstance()方法的源码,里面注释到反射不能破坏枚举类型Cannot reflectively create enum objects

    @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
    IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }
    

    所以我们可以通过枚举类型实现单例模式。

    package cn.itzhouq.single;
    
    /**
     * enum 其实本身也是一个 Class 类
     */
    public enum EnumSingle {
    
        INSTANCE;
    
        public EnumSingle getInstance() {
            return INSTANCE;
        }
    }
    
    class Test {
        public static void main(String[] args) {
            EnumSingle instance1 = EnumSingle.INSTANCE;
            EnumSingle instance2 = EnumSingle.INSTANCE;
    
            System.out.println(instance1 == instance2); // true
        }
    }
    

    没有问题,两次获得的同一个对象。这种通过枚举的方式还没有被广泛采用,但这是实现单例模式的最佳方式。 它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。不能通过 reflection attack 来调用私有构造方法。

    下面尝试使用反射破坏枚举类型。

    7、反射破坏枚举

    查看枚举类实现的源码,发现一个空参构造器,尝试使用反射获得空参构造器,然后创建单例的对象。

    枚举类的实现

    class Test {
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            EnumSingle instance1 = EnumSingle.INSTANCE;
            Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
            declaredConstructor.setAccessible(true);
            EnumSingle instance2 = declaredConstructor.newInstance();
            System.out.println(instance1 == instance2);
        }
    }
    
    Exception in thread "main" java.lang.NoSuchMethodException: cn.itzhouq.single.EnumSingle.<init>()
    	at java.lang.Class.getConstructor0(Class.java:3082)
    	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    	at cn.itzhouq.single.Test.main(EnumSingle.java:21)
    

    运行抛出了异常。但是异常信息显示没有这样的构造方法,这根反射不能破坏枚举类型的异常信息不一致。存在疑点,下面尝试使用反编译的方式查看源码分析。

    8、反编译枚举类

    反编译枚举类,分析代码实现。

    使用 JDK 自带的反编译工具编译代码

    发现代码中还是显示空参构造器。

    反编译

    这里反编译的命令是:

    javap -p EnumSingle.class
    

    使用 jad 工具反编译的工具:

    jad -s java EnumSingle.class
    Parsing EnumSingle.class... Generating EnumSingle.java
    

    查看反编译的代码:

    // 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:   EnumSingle.java
    
    package cn.itzhouq.single;
    
    
    public final class EnumSingle extends Enum
    {
    
        public static EnumSingle[] values()
        {
            return (EnumSingle[])$VALUES.clone();
        }
    
        public static EnumSingle valueOf(String name)
        {
            return (EnumSingle)Enum.valueOf(cn/itzhouq/single/EnumSingle, name);
        }
    
        private EnumSingle(String s, int i)
        {
            super(s, i);
        }
    
        public EnumSingle getInstance()
        {
            return INSTANCE;
        }
    
        public static final EnumSingle INSTANCE;
        private static final EnumSingle $VALUES[];
    
        static 
        {
            INSTANCE = new EnumSingle("INSTANCE", 0);
            $VALUES = (new EnumSingle[] {
                INSTANCE
            });
        }
    }
    

    可以看到 22 到 25 行,该枚举类并不是使用的无参构造器。而是使用的两个参数,根据这两个参数修改获取构造器的方法。

    class Test {
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            EnumSingle instance1 = EnumSingle.INSTANCE;
            Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
            declaredConstructor.setAccessible(true);
            EnumSingle instance2 = declaredConstructor.newInstance();
            System.out.println(instance1 == instance2); // true
        }
    }
    

    再看这次的异常信息:

    Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    	at cn.itzhouq.single.Test.main(EnumSingle.java:23)
    

    是符合上面的源码提示信息的。至此,关于的单例模式的安全性分析到此结束了。
    参考文章

  • 相关阅读:
    算法训练 表达式计算
    基础练习 十六进制转十进制
    基础练习 十六进制转十进制
    基础练习 十六进制转十进制
    New ways to verify that Multipath TCP works through your network
    TCP的拥塞控制 (Tahoe Reno NewReno SACK)
    Multipath TCP Port for Android 4.1.2
    How to enable ping response in windows 7?
    NS3
    Multipath TCP Port for Android
  • 原文地址:https://www.cnblogs.com/itzhouq/p/singleton.html
Copyright © 2011-2022 走看看