• 设计模式


    由浅入深分析单例模式

    单例模式(Singleton Patten):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例(Ensure a class has only one instance, and provide a global point of access to it)。

    Singleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实例,并且是自行实例化的(在Singleton中自己使用new Singleton())。

    1.单例入门

    饿汉式:在类加载时就创建对象实例,而不管实际是否需要创建。

    public class Singleton {
    
        private static final Singleton singleton = new Singleton();
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            return singleton;
        }
    }

    懒汉式:只有调用getInstance的时候,才实例化对象。

    public class Singleton {
    
        private static Singleton singleton;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            if (singleton == null) {
                singleton = new Singleton();
            }
            return singleton;
        }
    }

    问题:

    • 饿汉式没能做到延迟加载(lazy loading)。所谓延迟加载就是当真正需要数据的时候,才执行数据加载操作,为了避免一些无谓的性能开销。但饿汉式的好处是线程安全。
    • 上文中的懒汉式单例在多线程环境下可能会有多个进程同时通过(singleton == null)的条件检查。这样就创建出了多个实例,并且很可能造成内存泄露。

    2.多线程进阶

    方案1:

    在getInstance()方法上加synchronized关键字。

    public class Singleton {
    
        private static Singleton singleton;
    
        private Singleton() {
        }
    
        public static synchronized Singleton getInstance() {
            if (singleton == null) {
                singleton = new Singleton();
            }
            return singleton;
        }
    }

    问题:只有在单例初始化的时候我们才需要保证线程安全,其他时候方法上的synchronized关键字只会降低性能。

    方案2:

    只在单例初始化的时候加synchronized关键字。

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

    问题:还是有可能会有多个进程同时通过(singleton == null)的条件检查,进而创建多个实例。

    方案3:

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

    问题:和方案1类似,本来只是想让new这个操作并行,现在只要是进入getInstance()的线程都得同步,影响性能。

    方案4:

    双重检查加锁(Double-Check Lock)。

    public class Singleton {
    
        private static Singleton singleton = null;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    //如果被同步的线程中,有一个线程创建了对象,那么别的线程就不用再创建了
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }

    问题:DCL失效。

    DCL失效主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在JVM中这句话大概做了下面 3 件事情:

    • 给 singleton 分配内存。
    • 调用 Singleton 的构造函数来初始化成员变量,形成实例。
    • 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)。

    但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在3执行完毕、2未执行之前,如果另一个线程抢占了锁,这时 instance 已经是非 null 了(但却没有初始化),所以该线程会直接返回 instance,然后使用,然后顺理成章地报错。

    方案5(最终版):

    加volatile。

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

    使用 volatile 有两个作用:

    • 这个变量不会在多个线程中保存复本,而是直接从内存读取。

    • 这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。

    3.还能更好吗

    老版《Effective Java》中推荐的方式:

    public class Singleton {
    
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        private Singleton() {
        }
    
        public static final Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }

    上面这种方式,仍然使用JVM本身的机制保证了线程安全问题:

    • 由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它只有在getInstance()被调用时才会真正创建。
    • 同时读取实例的时候不会进行同步,没有性能缺陷。
    • 不依赖 JDK 版本。

    枚举实现:

    public enum Singleton {
    
        INSTANCE;
    
        public void doSomething() {
        }
    }

    默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。但是在枚举中的其他方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。

    这个版本基本上消除了绝大多数的问题,代码也非常简单,是新版的《Effective Java》中推荐的模式。

    4.其他问题

    序列化攻击:

    单例实现采用方案5,序列化攻击代码如下:

    public class Main {
    
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            //序列化方式破坏单例   测试
            serializeDestroyMethod();
        }
    
        private static void serializeDestroyMethod() throws IOException, ClassNotFoundException {
            Singleton singleton;
            Singleton singletonNew;
    
            singleton = Singleton.getInstance();
    
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(singleton);
    
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            singletonNew = (Singleton) ois.readObject();
    
            System.out.println(singleton == singletonNew);
        }
    }

    打印结果为:false。在单例类中添加一个方法 readResolve():

    public class Singleton implements Serializable {
        private volatile static Singleton singleton = null;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    
        private Object readResolve() {
            return singleton;
        }
    }

    再执行攻击代码,打印结果为:true。

    反序列化攻击源码分析:

        //默认情况下 该方法通过反射创建一个新对象并返回
        private Object readOrdinaryObject(boolean unshared)
                throws IOException {
            if (bin.readByte() != TC_OBJECT) {
                throw new InternalError();
            }
    
            ObjectStreamClass desc = readClassDesc(false);
            desc.checkDeserialize();
    
            Class<?> cl = desc.forClass();
            if (cl == String.class || cl == Class.class
                    || cl == ObjectStreamClass.class) {
                throw new InvalidClassException("invalid class descriptor");
            }
    
            Object obj;
            try {
                obj = desc.isInstantiable() ? desc.newInstance() : null;
            } catch (Exception ex) {
                throw (IOException) new InvalidClassException(
                        desc.forClass().getName(),
                        "unable to create instance").initCause(ex);
            }
    
            passHandle = handles.assign(unshared ? unsharedMarker : obj);
            ClassNotFoundException resolveEx = desc.getResolveException();
            if (resolveEx != null) {
                handles.markException(passHandle, resolveEx);
            }
    
            if (desc.isExternalizable()) {
                readExternalData((Externalizable) obj, desc);
            } else {
                readSerialData(obj, desc);
            }
    
            handles.finish(passHandle);
    
            //经过上面的代码,新对象已经被new出来了
            //下面hasReadResolveMethod()这个方法很关键。如果该类存在readResolve()方法,就调用该方法返回的实例替换掉新创建的对象。如果不存在就直接把new出来的对象返回出去。
            if (obj != null &&
                    handles.lookupException(passHandle) == null &&
                    desc.hasReadResolveMethod()) {
                Object rep = desc.invokeReadResolve(obj);
                if (unshared && rep.getClass().isArray()) {
                    rep = cloneArray(rep);
                }
                if (rep != obj) {
                    // Filter the replacement object
                    if (rep != null) {
                        if (rep.getClass().isArray()) {
                            filterCheck(rep.getClass(), Array.getLength(rep));
                        } else {
                            filterCheck(rep.getClass(), -1);
                        }
                    }
                    handles.setObject(passHandle, obj = rep);
                }
            }
    
            return obj;
        }
    
    ...
    /** * 返回该类是否有readResolve方法 */ boolean hasReadResolveMethod() { requireInitialized(); return (readResolveMethod != null); }

    所以在单例类中添加方法 readResolve(),就可以防范反序列化攻击。

    反射攻击:

    单例实现依然采用方案5,反射攻击代码如下:

    public class Main {
    
        public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
            reflexDestroyMethod();
        }
    
        private static void reflexDestroyMethod() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            Class objectClass = Singleton.class;
            Constructor constructor = objectClass.getDeclaredConstructor();
            constructor.setAccessible(true);
    
            Singleton singleton = Singleton.getInstance();
            Singleton singletonNew = (Singleton) constructor.newInstance();
    
            System.out.println(singleton == singletonNew);
        }
    }

    打印结果为:false。

    使用枚举实现单例后,执行反射攻击报错如下:

    原因是Singleton.class.getDeclaredConstructors()获取所有构造器,会发现并没有我们所设置的无参构造器,只有一个参数为(String.class,int.class)构造器。看下Enum源码就明白,这两个参数是name和ordinal两个属性:

    public abstract class Enum<E extends Enum<E>>
                implements Comparable<E>, Serializable {
            private final String name;
            public final String name() {
                return name;
            }
            private final int ordinal;
            public final int ordinal() {
                return ordinal;
            }
            protected Enum(String name, int ordinal) {
                this.name = name;
                this.ordinal = ordinal;
            }
            //余下省略

    枚举Enum是个抽象类,一旦一个类声明为枚举,实际上就是继承了Enum,所以就会有(String.class,int.class)的构造器。既然无参构造方法找不到,那我们就使用父类Enum的构造器,看看是什么情况:

    public class Main {
    
        public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
            //反射方式破坏单例 测试
            reflexDestroyMethod();
        }
    
        private static void reflexDestroyMethod() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            Class objectClass = SingletonE.class;
            Constructor constructor = objectClass.getDeclaredConstructor(String.class, int.class);
            constructor.setAccessible(true);
    
            SingletonE singleton = SingletonE.INSTANCE;
            SingletonE singletonNew = (SingletonE) constructor.newInstance("test", 1);
    
            System.out.println(singleton == singletonNew);
        }
    }

    执行结果如下:

    说是不能反射创建枚举对象,newInstance()方法源码如下:

        @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;
        }

    由上文源码可知,反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。

    总结:

    单元素的枚举类型已经成为实现Singleton的最佳方法 —— 《Effective Java》

  • 相关阅读:
    C#面试题
    深入浅出JSONP--解决ajax跨域问题
    vs切换当前编辑文件时自动定位目录树
    测试从应用到DB的准确的网络延迟
    MySQL死锁检测和回滚
    [磁盘空间]lsof处理文件恢复、句柄以及空间释放问题
    [硬件知识]OP(Over-provisioning)预留空间
    查看实例上面无主键的表
    mysql replace语句
    理解innodb buffer pool
  • 原文地址:https://www.cnblogs.com/helios-fz/p/11400105.html
Copyright © 2020-2023  润新知