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

栏目: 后端 · 发布时间: 5年前

内容简介:它的原理是利用了类加载机制。执行这段代码会发现o1<>o2,这就破坏了单例。为什么呢?罪魁祸首就是如下代码,它是反射的newInstance()的底层实现。

一、静态内部类

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架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)

为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!希望读到这的您能点个小赞和关注下我,以后还会更新技术干货,谢谢您的支持!

资料领取方式:加入Java技术交流群 963944895点击加入群聊 ,私信管理员即可免费领取


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

大演算

大演算

佩德羅.多明戈斯 / 張正苓,胡玉城 / 三采 / 2016-8-1 / 620

揭開大數據、人工智慧、機器學習的祕密, 打造人類文明史上最強大的科技——終極演算法! 有一個終極演算法,可以解開宇宙所有的祕密, 現在大家都在競爭,誰能最先解開它! .機器學習是什麼?大演算又是什麼? .大演算如何運作與發展,機器可以預測什麼? .我們可以信任機器學過的東西嗎? .商業、政治為什麼要擁抱機器學習? .不只商業與政治,醫學與科學界也亟需......一起来看看 《大演算》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具