美文网首页互联网科技Java
深入理解《单例模式》之源码分析

深入理解《单例模式》之源码分析

作者: Java_苏先生 | 来源:发表于2019-05-15 20:33 被阅读1次

    一、静态内部类

    public class InnerClassSingleton implements Serializable {
        
        //无参构造函数
        private InnerClassSingleton(){};
        
        public static final InnerClassSingleton getInstance(){
            return InnerClassHelper.INSTANCE;
        }
        
        //内部类
        private static class InnerClassHelper{
            private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
        }
    }
    

    它的原理是利用了类加载机制。

    1.1、但是它可以被反射破坏

        Class clazz = InnerClassSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(null);
            c.setAccessible(true);
            Object o1 = c.newInstance();
    
            Object o2 = InnerClassSingleton.getInstance();
    

    执行这段代码会发现o1<>o2,这就破坏了单例。
    为什么呢?罪魁祸首就是如下代码,它是反射的newInstance()的底层实现。

    UnsafeFieldAccessorImpl.unsafe.allocateInstance(class)
    

    我们知道new创建对象时会被编译成3条指令:

    • 根据类型分配一块内存区域
    • 把第一条指令返回的内存地址压入操作数栈顶
    • 调用类的构造函数

    而Unsafe.allocateInstance()方法值做了第一步和第二步,即分配内存空间,返回内存地址,没有做第三步调用构造函数。所以Unsafe.allocateInstance()方法创建的对象都是只有初始值,没有默认值也没有构造函数设置的值,因为它完全没有使用new机制,绕过了构造函数直接操作内存创建了对象,而单例是通过私有化构造函数来保证的,这就使得单例失败。

    1.2、还可以被反序列化破坏

    InnerClassSingleton o1 = null;
    InnerClassSingleton o2 = InnerClassSingleton.getInstance();
    
    FileOutputStream fos = new FileOutputStream("InnerClassSingleton.obj");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    oos.writeObject(o2);
    oos.flush();
    oos.close();
    
    FileInputStream fis = new FileInputStream("InnerClassSingleton.obj");
    ObjectInputStream ois = new ObjectInputStream(fis);
    o1 = (InnerClassSingleton) ois.readObject();
    ois.close();
    
    System.out.println(o1);
    System.out.println(o2);
    

    执行完这段代码我们又会发现o1<>o2,可见通过反序列化,成功破坏了单例,创建了2个对象。
    那么如何避免这种情况发生呢?很简单,只要在代码中添加:

    public class InnerClassSingleton implements Serializable {
        ....省略重复代码
        private Object readResolve(){
            return InnerClassHelper.INSTANCE;
        }
    }
    

    这时候我们可以再执行一下上面反序列化的方法,会很神奇的发现o1==o2,那这是为什么呢?我们一起来看下ois.readObject()的源码:

    private Object readObject0(boolean unshared) throws IOException {
        ...省略
        case TC_OBJECT:
          return checkResolve(readOrdinaryObject(unshared));
    }
    -------------------------------------------------------------------
    private Object readOrdinaryObject(boolean unshared){
        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 {
        //重点!!!
        //首先isInstantiable()判断是否可以初始化
        //如果为true,则调用newInstance()方法创建对象,这时创建的对象是不走构造函数的,是一个新的对象
                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);
        
        //重点!!!
        //hasReadResolveMethod()会去判断,我们的InnerClassSingleton对象中是否有readResolve()方法
            if (obj != null &&
                handles.lookupException(passHandle) == null &&
                desc.hasReadResolveMethod())
            {
        //如果为true,则执行readResolve()方法,而我们在自己的readResolve()方法中 直接retrun InnerClassHelper.INSTANCE,所以还是返回的同一个对象,保证了单例
                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;
    }
    

    最后总结一下静态内部类写法:

    优点:不用synchronized,性能好;简单
    缺点:无法避免被反射、反序列化破坏

    二、枚举

    public enum EnumSingleton {
        
        INSTANCE;
    
        private Object data;
    
        public Object getData() {
            return data;
        }
    
        public void setData(Object data) {
            this.data = data;
        }
    
        public static EnumSingleton getInstance() {
            return INSTANCE;
        }
    }
    

    反编译这段代码,得到:

        static
            {
                INSTANCE = new EnumSingleton("INSTANCE",0);
                $VALUE = (new EnumSingleton[] {
                        INSTANCE
                });
            }
    

    显然这是一种饿汉式的写法,用static代码块来保证单例(在类加载的时候就初始化了)。

    2.1、可以避免被反射破坏

    //反射
    Class clazz = EnumSingleton.class;
    //拿到构造函数
    Constructor c = clazz.getDeclaredConstructor(String.class, int.class);
    c.setAccessible(true);
    EnumSingleton instance1 = (EnumSingleton)c.newInstance("smart", 111);
    -----------------------------------------------------------------------------------------
    public T newInstance(Object ... initargs){
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
           throw new IllegalArgumentException("Cannot reflectively create enum objects");
    } 
    

    可以看到,在newInstance()方法中,做了类型判断,如果是枚举类型,直接抛出异常。也就是说从jdk层面保证了枚举不能被反射。

    2.2、可以避免被反序列化破坏

    Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。

    ...省略
    EnumSingleton o1 = (EnumSingleton) ois.readObject();
    -----------------------------------------------------------------------------------
    private Object readObject0(boolean unshared) throws IOException {
        ...省略
        case TC_ENUM:
          return checkResolve(readEnum(unshared));
    }
    -------------------------------------------------------------------
    private Object readEnum(boolean unshared){
        ...省略
        String name = readString(false);
            Enum<?> result = null;
            Class<?> cl = desc.forClass();
            if (cl != null) {
                try {
                    @SuppressWarnings("unchecked")
            //重点!!!
            //通过valueOf方法获取Enum,参数为class和name
                    Enum<?> en = Enum.valueOf((Class)cl, name);
                    result = en;
                } catch (IllegalArgumentException ex) {
                    throw (IOException) new InvalidObjectException(
                        "enum constant " + name + " does not exist in " +
                        cl).initCause(ex);
                }
                if (!unshared) {
                    handles.setObject(enumHandle, result);
                }
            }
    }
    

    所以序列化的时候只将 INSTANCE 这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

    三、ThreadLocal单例模式

    public class Singleton {
        
        private Singleton(){}
        
        private static final ThreadLocal<Singleton> threadLocal = 
                new ThreadLocal<Singleton>(){
                    @Override
                    protected Singleton initialValue(){
                        return new Singleton();
                    }
                };
        
        public static Singleton getInstance(){
            return threadLocal.get();
        }
        
    }
    

    这种写法利用了ThreadLocal的特性,可以保证局部单例,即在各自的线程中是单例的,但是线程与线程之间不保证单例。

    应用场景(在Spring的第三方包baomidou的多数据源中,有用到这种写法)

    package com.baomidou.dynamic.datasource.toolkit;
    import java.util.concurrent.LinkedBlockingDeque;
    
    public final class DynamicDataSourceContextHolder {
        //重点!!!
        private static final ThreadLocal<LinkedBlockingDeque<String>> LOOKUP_KEY_HOLDER = new ThreadLocal() {
            protected Object initialValue() {
                return new LinkedBlockingDeque();
            }
        private DynamicDataSourceContextHolder() {
        }
    
        public static String getDataSourceLookupKey() {
            LinkedBlockingDeque<String> deque = (LinkedBlockingDeque)LOOKUP_KEY_HOLDER.get();
            return deque.isEmpty() ? null : (String)deque.getFirst();
        }
    
        public static void setDataSourceLookupKey(String dataSourceLookupKey) {
            ((LinkedBlockingDeque)LOOKUP_KEY_HOLDER.get()).addFirst(dataSourceLookupKey);
        }
    
        public static void clearDataSourceLookupKey() {
            LinkedBlockingDeque<String> deque = (LinkedBlockingDeque)LOOKUP_KEY_HOLDER.get();
            if (deque.isEmpty()) {
                LOOKUP_KEY_HOLDER.remove();
            } else {
                deque.pollFirst();
            }
        }
        };
    }
    

    PS:initialValue()一般是用来在使用时进行重写的,如果在没有set的时候就调用get,会调用initialValue方法初始化内容。

    写在最后

    点关注,不迷路;Java苏先生每天更新Java相关技术及资讯

    相关文章

      网友评论

        本文标题:深入理解《单例模式》之源码分析

        本文链接:https://www.haomeiwen.com/subject/bjnmaqtx.html