从单例模式到字节码、反射及序列化

  1. 应用场景

    单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

  2. 饿汉式单例

    饿汉式单例是在类加载的时候创建单例对象,不存在线程安全问题,获取单例效率高,体验更好。缺点是如果系统中没有用到这个单例,它会一直占用内存空间,浪费了内存空间。饿汉式单例模式代码如下所示:

    package com.lx.learning.designpattern.singleton.hungry;
    
    public class HungrySingleton {
    	//在类加载的时候单例便创建了
        private static final HungrySingleton instance=new HungrySingleton();
    
        private HungrySingleton() {
        }
    
        public static HungrySingleton getInstance(){
            return instance;
        }
    
    }
    复制代码
  3. 懒汉式单例

    顾名思义,懒汉式单例就是在需要用到的时候再创建单例对象,这样便可以解决不使用单例也占空间的问题,代码如下所示。但是下面这个单例写法会产生线程安全问题,在new LazySingleton()的时候需要时间,单例在创建的过程中有可能instance为空,这个时候有别的线程来访问getInstance()的话也可以进入创建单例的代码块,这样“单例”就被多次创建,出现线程安全问题。

    package com.lx.learning.designpattern.singleton.lazy;
    
    public class LazySingleton {
    
        private static LazySingleton instance = null;
    
        private LazySingleton() { }
    
        public static LazySingleton getInstance() {
            if (instance == null) {
                //有可能多个线程进入这个代码块
                instance = new LazySingleton();
            }
            return instance;
        }
    }
    复制代码

    那么该怎么解决这个问题呢,最简单粗暴的方法是在getInstance()方法前加上synchronized关键字,使获取单例的方法只有一个线程能访问。但是这么做的话,会出现性能问题,在高并发的场景下会导致大量的线程阻塞,导致系统性能下降。双重检查锁机制便可以解决线程安全和性能问题,代码如下所示。第一个if判断语句的作用是单例已经创建好的情况下,线程不用在进入同步块了。第二个if判断语句的作用是单例还没创建的情况下保证只用一个线程能创建单例。这样便实现了需要时创建和线程安全的单例模式。

    package com.lx.learning.designpattern.singleton.lazy;
    
    public class LazySingleton {
    
        private static LazySingleton instance = null;
    
        private LazySingleton() {
        }
    
        public static LazySingleton getInstance() {
            if (instance == null) {
                synchronized (LazySingleton.class) {
                    if (instance == null) {
                        instance = new LazySingleton();
                    }
                }
            }
            return instance;
        }
    }
    复制代码

    还有一种更好的懒汉式单例,就是使用静态内部类来实现。这个跟静态内部类的加载时机有很大关系,没有使用到内部类时候不会加载,在使用到时如果没加载静态内部类会先加载和初始化,在调用响应的方法。在使用常量LazyHolder.instance时,如果LazyHolder类没有加载,则会先加载该类,在加载的过程中会先创建单例对象。下次调用如果在方法区有该类的信息则不会加载了,这个方式也是单例懒汉式和线程安全比较理想的实现方式,代码如下。

    package com.lx.learning.designpattern.singleton.lazy;
    
    public class LazyInnerClassSingleton {
    
        private LazyInnerClassSingleton() {
        }
    
        public LazyInnerClassSingleton getInstance(){
            return LazyHolder.instance;
        }
    
        private static class LazyHolder{
            private static final LazyInnerClassSingleton instance=new LazyInnerClassSingleton();
        }
    }
    复制代码
  4. 枚举式单例

    通过枚举来实现实现单例,属于饿汉式单例,枚举在加载的时候就把对象创建好了,是单例的一种比较好的实现方式。

     package com.lx.learning.designpattern.singleton.enumeration;
    
     public enum EnumSingleton {
         INSTANCE;
         private String name;
    
         public void test() {
             System.out.println(name);
         }
    
         public void setName(String name) {
             this.name = name;
         }
     }
    复制代码

    使用javap -v EnumSingleton.class命令查看枚举的静态代码块字节码如下所示,invokespecial指令的作用是调用实例构造器方法、私有方法和父类方法。这段字节码的整体含义是在类初始化的时候创建了EnumSingleton对象,并把这个对象赋值给了INSTANCE变量。如果这块内容看不懂,我将会在JVM的相关内容中详细说明,敬请关注。

    static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=4, locals=0, args_size=0
             0: new           #4                  // class com/lx/learning/designpattern/singleton/enumeration/EnumSingleton 创建一个对象并将其引用值压入栈顶
             3: dup
             4: ldc           #10                 // String INSTANCE
             6: iconst_0                          //把常量0推送至栈顶(INSTANCE)
             7: invokespecial #11                 // Method "<init>":(Ljava/lang/String;I)V
            10: putstatic     #12                 // Field INSTANCE:Lcom/lx/learning/designpattern/singleton/enumeration/EnumSingleton;
            13: iconst_1
            14: anewarray     #4                  // class com/lx/learning/designpattern/singleton/enumeration/EnumSingleton
            17: dup
            18: iconst_0
            19: getstatic     #12                 // Field INSTANCE:Lcom/lx/learning/designpattern/singleton/enumeration/EnumSingleton;
            22: aastore
            23: putstatic     #1                  // Field $VALUES:[Lcom/lx/learning/designpattern/singleton/enumeration/EnumSingleton;
            26: return
          LineNumberTable:
            line 4: 0
            line 3: 13
    复制代码
  5. 容器式单例

    把所有的单例对象都放在一个容器中,更加方便管理。Spring中的容器式单例就是采用的这种方式。关于容器式线程安全问题,采用的是双重检查机制来处理的(参考第3点中懒汉式的双重检查机制),代码如下。

    package com.lx.learning.designpattern.singleton.container;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    public class ContainerSingleton {
    
        private static Map<String, Object> container = new ConcurrentHashMap<>();
    
        private ContainerSingleton() {
        }
    
        private static Object getInstance(String name) {
            //如果容器中包含要去的单例则直接返回不创建单例了,这里使用是双重检测机制
            if (!container.containsKey(name)) {
                synchronized (ContainerSingleton.class){
                    if (!container.containsKey(name)) {
                        try {
                            Class clazz = Class.forName(name);
                            Object obj = clazz.newInstance();
                            container.put(name, obj);
                            return obj;
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            return container.get(name);
        }
    
        public static void main(String[] args) {
            Object obj=getInstance("com.lx.learning.designpattern.singleton.container.User");
            System.out.println(obj);
        }
    }
    复制代码
  6. 每个线程中的单例,保证线程内部的全局唯一。通过ThreadLocal来实现,ThreadLocalSingleton代码如下所示:

    package com.lx.learning.designpattern.singleton.thread;
    
    public class ThreadLocalSingleton {
    
        private ThreadLocalSingleton(){
        }
    
        private static final ThreadLocal<ThreadLocalSingleton> instance=
                ThreadLocal.withInitial(ThreadLocalSingleton::new);
    
        public static ThreadLocalSingleton getInstance(){
            return instance.get();
        }
    }
    复制代码
  7. 单例的破坏

    (1)如果通过反射的方式来获取对象会不会破坏单例模式呢,我们可以通过下面的代码测试一下:

    package com.lx.learning.designpattern.singleton.test;
    
    import com.lx.learning.designpattern.singleton.hungry.HungrySingleton;
    import java.lang.reflect.Constructor;
    
    public class ReflectSingleton {
    
        public static void main(String[] args) {
            Class<HungrySingleton> clazz = HungrySingleton.class;
            try {
                //1.获取单例的构造器
                Constructor<HungrySingleton> constructor = clazz.getDeclaredConstructor();
                //2.因为构造器为私有的,所以要设置成私有可以被反射访问
                constructor.setAccessible(true);
                //3.通过反射生成两个对象
                HungrySingleton singleton1 = constructor.newInstance();
                HungrySingleton singleton2 = constructor.newInstance();
                //4.输出这两个对象的HashCode
                System.out.println("Singleton1 hashCode==>" + singleton1.hashCode());
                System.out.println("Singleton2 hashCode==>" + singleton2.hashCode());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    复制代码

    运行结果如下,HashCode不同说明生成了两个不同的对象,单例模式就被破坏了。

    Singleton1 hashCode==>1956725890
    Singleton2 hashCode==>356573597
    复制代码

    怎样防止反射破坏单例模式呢,通过反射调用newInstance方法时会调用构造器来实例化对象,因此在构造器中做实例是否为空的判断,即可防止单例被反射破坏,下面方案只适用于饿汉式和静态内部类的方式,代码如下所示。

    package com.lx.learning.designpattern.singleton.hungry;
    
    public class HungrySingleton {
    
       private static final HungrySingleton instance = new HungrySingleton();
    
       private HungrySingleton() {
           if (instance != null){
               throw  new RuntimeException("单例已存在,不允许重新创建");
           }
       }
    
       public static HungrySingleton getInstance() {
           return instance;
       }
    
    }
    复制代码

    (2)通过序列化的方式来生成对象会破坏单例模式,步骤如下所示。

    package com.lx.learning.designpattern.singleton.serial;
    
    import com.lx.learning.designpattern.singleton.hungry.HungrySingleton;
    
    public class SingletonSerialTest {
        public static void main(String[] args) {
            HungrySingleton singleton = HungrySingleton.getInstance();
            //1.把对象序列化成字节数组
            byte[] bytes = SerialUtil.serial(singleton);
            //2.把字节数组转换成对象
            HungrySingleton hungrySingleton1 = SerialUtil.deSerial(bytes);
            HungrySingleton hungrySingleton2 = SerialUtil.deSerial(bytes);
            //3.比较两个对象是否相等
            System.out.println(hungrySingleton1 == hungrySingleton2);
        }
    }
    复制代码

    SerialUtil工具类代码如下:

    package com.lx.learning.designpattern.singleton.serial;
    
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    
    public class SerialUtil {
    
        public static <T> byte[] serial(T object) {
            try {
                ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
                ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
                objectOutputStream.writeObject(object);
                return byteArrayOutputStream.toByteArray();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public static <T>  T deSerial(byte[] data) {
            try {
                ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(data);
                ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream);
                return (T)objectInputStream.readObject();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
    }
    
    复制代码

    运行结果为false,说明单例可以通过反序列化的手段来破坏。那么如果来防止通过序列化的手段来破坏单例呢。可以通过序列化中readResolve()方法来防止,只需要在该方法中调用获取单例的方法即可。java在反序列化时会先通过反射获取对象的readResolve方法,如果该方法不为空则通过该方法来生成对象,否则生成新的对象。防止序列化破坏单例的代码如下所示:

    package com.lx.learning.designpattern.singleton.hungry;
    
    import java.io.Serializable;
    
    public class HungrySingleton implements Serializable {
    
        private static final HungrySingleton instance = new HungrySingleton();
    
        private HungrySingleton() {
            if (instance != null){
                throw  new RuntimeException("单例已存在,不允许重新创建");
            }
        }
    
        public static HungrySingleton getInstance() {
            return instance;
        }
    
        private Object readResolve(){
            return getInstance();
        }
    
    }
    复制代码

    (3)通过枚举实现的单例能否被反射和序列化技术破坏呢,下面先看通过反射来获取枚举单例,代码如下:

    package com.lx.learning.designpattern.singleton.test;
    
    import com.lx.learning.designpattern.singleton.enumeration.EnumSingleton;
    import java.lang.reflect.Constructor;
    
    public class ReflectEnumSingleton {
        public static void main(String[] args) {
            Class<EnumSingleton> clazz = EnumSingleton.class;
            try {
                //1.获取单例的构造器
                Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor();
                //2.因为构造器为私有的,所以要设置成私有可以被反射访问
                constructor.setAccessible(true);
                //3.通过反射生成两个对象
                EnumSingleton singleton1 = constructor.newInstance();
                EnumSingleton singleton2 = constructor.newInstance();
                //4.输出这两个对象的HashCode
                System.out.println("Singleton1 hashCode==>" + singleton1.hashCode());
                System.out.println("Singleton2 hashCode==>" + singleton2.hashCode());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    复制代码

    通用反射时发现抛出了java.lang.NoSuchMethodException: com.lx.learning.designpattern.singleton.enumeration.EnumSingleton.<init>()异常说明不能通过反射来创建枚举实例。查看Enum类的源码,发现有一个Enum(String name, int ordinal)构造方法,如下所示。

    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    复制代码

    那么来看看能不能通过这个父类的方法来实例化枚举单例,代码如下。

    package com.lx.learning.designpattern.singleton.test;
    
    import com.lx.learning.designpattern.singleton.enumeration.EnumSingleton;
    import java.lang.reflect.Constructor;
    
    public class ReflectEnumSingleton2 {
        public static void main(String[] args) {
            Class<EnumSingleton> clazz = EnumSingleton.class;
            try {
                //1.获取单例的构造器
                Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class,int.class);
                //2.因为构造器为私有的,所以要设置成私有可以被反射访问
                constructor.setAccessible(true);
                //3.通过反射生成两个对象
                EnumSingleton singleton1 = constructor.newInstance("lx",0);
                EnumSingleton singleton2 = constructor.newInstance("lx",0);
                //4.输出这两个对象的HashCode
                System.out.println("Singleton1 hashCode==>" + singleton1.hashCode());
                System.out.println("Singleton2 hashCode==>" + singleton2.hashCode());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    复制代码

    运行后出现了java.lang.IllegalArgumentException: Cannot reflectively create enum objects异常,再次说明不能通过反射来创建枚举。因此通过反射不能破坏枚举单例。那么为什么反射不能创建枚举呢,在Constructor类中找到了答案,源码如下。

        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)
            	//如果是枚举类型则抛出IllegalArgumentException异常
                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;
        }
    复制代码

    如果反射获取到的类型是ENUMthrow new IllegalArgumentException("Cannot reflectively create enum objects");从源码层面更加证明了枚举不能通过反射技术来创建。

    (4)接下来我们看看通过序列化技术能否创建枚举单例,此处序列化代码和上文提到的序列化代码逻辑一样,代码如下。

    package com.lx.learning.designpattern.singleton.serial;
    
    import com.lx.learning.designpattern.singleton.enumeration.EnumSingleton;
    
    public class EnumSingletonSerialTest {
        public static void main(String[] args) {
            EnumSingleton singleton = EnumSingleton.INSTANCE;
            byte[] bytes = SerialUtil.serial(singleton);
            EnumSingleton hungrySingleton1 = SerialUtil.deSerial(bytes);
            EnumSingleton hungrySingleton2 = SerialUtil.deSerial(bytes);
            System.out.println(hungrySingleton1 == hungrySingleton2);
        }
    }
    复制代码

    运行后的结果为true这让我感到非常奇怪,枚举中也没有写readResolve方法,为什么反序列化之后是同一个对象呢。后来我在反序列的源码中找到了答案,是通过ObjectInputStream类的readObject()方法把字节数组转换成了对象。这个方法调用了该类的readObject0()方法,如下所示。

    private Object readObject0(boolean unshared) throws IOException {
            boolean oldMode = bin.getBlockDataMode();
            if (oldMode) {
                int remain = bin.currentBlockRemaining();
                if (remain > 0) {
                    throw new OptionalDataException(remain);
                } else if (defaultDataEnd) {
                    /*
                     * Fix for 4360508: stream is currently at the end of a field
                     * value block written via default serialization; since there
                     * is no terminating TC_ENDBLOCKDATA tag, simulate
                     * end-of-custom-data behavior explicitly.
                     */
                    throw new OptionalDataException(true);
                }
                bin.setBlockDataMode(false);
            }
    
            byte tc;
            while ((tc = bin.peekByte()) == TC_RESET) {
                bin.readByte();
                handleReset();
            }
    
            depth++;
            totalObjectRefs++;
            try {
                switch (tc) {
                    case TC_NULL:
                        return readNull();
    
                    case TC_REFERENCE:
                        return readHandle(unshared);
    
                    case TC_CLASS:
                        return readClass(unshared);
    
                    case TC_CLASSDESC:
                    case TC_PROXYCLASSDESC:
                        return readClassDesc(unshared);
    
                    case TC_STRING:
                    case TC_LONGSTRING:
                        return checkResolve(readString(unshared));
    
                    case TC_ARRAY:
                        return checkResolve(readArray(unshared));
    				//如果类型是枚举则用readEnum方法来进行反序列化
                    case TC_ENUM:
                        return checkResolve(readEnum(unshared));
    
                    case TC_OBJECT:
                        return checkResolve(readOrdinaryObject(unshared));
    
                    case TC_EXCEPTION:
                        IOException ex = readFatalException();
                        throw new WriteAbortedException("writing aborted", ex);
    
                    case TC_BLOCKDATA:
                    case TC_BLOCKDATALONG:
                        if (oldMode) {
                            bin.setBlockDataMode(true);
                            bin.peek();             // force header read
                            throw new OptionalDataException(
                                bin.currentBlockRemaining());
                        } else {
                            throw new StreamCorruptedException(
                                "unexpected block data");
                        }
    
                    case TC_ENDBLOCKDATA:
                        if (oldMode) {
                            throw new OptionalDataException(true);
                        } else {
                            throw new StreamCorruptedException(
                                "unexpected end of block data");
                        }
    
                    default:
                        throw new StreamCorruptedException(
                            String.format("invalid type code: %02X", tc));
                }
            } finally {
                depth--;
                bin.setBlockDataMode(oldMode);
            }
        }
    复制代码

    从上面的代码逻辑中可以看到,如果类型是枚举则用readEnum方法来进行反序列化,该方法源码如下所示。

     private Enum<?> readEnum(boolean unshared) throws IOException {
            if (bin.readByte() != TC_ENUM) {
                throw new InternalError();
            }
    
            ObjectStreamClass desc = readClassDesc(false);
            if (!desc.isEnum()) {
                throw new InvalidClassException("non-enum class: " + desc);
            }
    
            int enumHandle = handles.assign(unshared ? unsharedMarker : null);
            ClassNotFoundException resolveEx = desc.getResolveException();
            if (resolveEx != null) {
                handles.markException(enumHandle, resolveEx);
            }
    
            String name = readString(false);
            Enum<?> result = null;
            Class<?> cl = desc.forClass();
            if (cl != null) {
                try {
                    @SuppressWarnings("unchecked")
                    //最终枚举是从这里面获取的
                    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);
                }
            }
    
            handles.finish(enumHandle);
            passHandle = enumHandle;
            return result;
        }
    复制代码

    其中有一段关键代码Enum<?> en = Enum.valueOf((Class)cl, name);这段代码就是从一个枚举容器enumConstantDirectory中获取枚举实例,和上文字节码分析中枚举再加载的时候便会进行初始化相对应。也就是说枚举在加载的时候便把所有实例存储到了enumConstantDirectory容器中,用到时每次都从容器里拿,因此反序列化不会破坏枚举单例。valueOf方法源码如下。

    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                    String name) {
        	//从enumConstantDirectory容器中获取枚举实例
            T result = enumType.enumConstantDirectory().get(name);
            if (result != null)
                return result;
            if (name == null)
                throw new NullPointerException("Name is null");
            throw new IllegalArgumentException(
                "No enum constant " + enumType.getCanonicalName() + "." + name);
        }
    复制代码
  8. 单例模式总结

    (1)单例模式优点

    内存中只有一个实例,减少了内存开销
    可以避免对资源的多重使用
    设置全局访问点,严格控制访问
    复制代码

    (2)缺点

    没有接口,扩展困难
    复制代码

    (3)比较理想的单例模式

    懒汉式(双重检查锁机)
    通过内部静态类
    通过枚举来实现
    复制代码

    本文详细讲解了多种单例的实现方式,其中有饿汉式、懒汉式、容器式单例。相对应不同的场景应该考虑不同的,有的单例用到的概率非常大,那么考虑用饿汉式,比如spring中的配置文件信息封装和各种各样的上下文context。这些信息一定会用到,用饿汉式单例比较合适,也不存在线程安全问题。本文还通过反射和序列化技术来探究单例被破坏的情况,并提供了解决方案,遂把这些内容记录下来。

    源码地址,欢迎Star(^▽^)!

转载于:https://juejin.im/post/5c8f0e2ce51d4513b9072604

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值