专栏首页一块自留地面试官问我单例模式真的安全吗?我懵逼了

面试官问我单例模式真的安全吗?我懵逼了

某天晚上,我和基友正在开黑排位,刚刚被敌方EZ用E躲了我混分巨兽的石破天惊,恼羞成怒的我正准备咬牙切齿还回去时,手机响了。

于是不耐烦的接通了电话,正准备来一套祖安素质3连,结果电话那边传来:“你好,请问是XXX吗?我是某大厂的面试官,现在方便进行面试吗?”

听到这里,吓得我鼠标一抖,赶快公屏输入gg准备迎接面试。 兄弟们对不住,先挂机一会哈,争取10分钟搞定面试官再回来拯救你们,一定要撑住!

我自信一笑,对面试官说:“可以开始了。”

面试官:“那我就开始了啊,看你简历上说使用过单例模式,那我们来聊聊单例吧”

卧槽,啥情况,这节奏不对啊,怎么连前戏都没有,按套路不是应该先自我介绍吗?

那我精心准备的中日双语自我介绍岂不是派不上用场了?

算了,不慌,提好裤子稳得住。

面试官:“那先说下你什么情况下用的单例模式吧,怎么用的”

切,还以为一上来要放什么大招,也不过如此。想到这里,我不禁桀桀一笑

单例模式我是创建消息队列的生产者的时候使用的,用来保证生产者全局只有一个实例

用的是双重校验检查的方式。

面试官:“哦?为什么要保证生产者全局只有一个实例呢?什么是双重校验检查?”

哈,就知道你要问这个,老夫早有准备。

是因为我们要在多线程中调用生产者投递消息,如果每次都new一个新的生产者,没有必要,而且会造成内存泄漏。

至于双重校验检查嘛,额,老哥你稍等下,我给你手写一下,然后微信给你发过去哈,对了老哥,你微信号是多少呀?我加一下。

面试官:“。。。”

哈哈,开玩笑得啦,我可是正经的直男,不会对你gay兴趣的,阿呸,感兴趣,放轻松~ 写法如下:

public class SingleTon {
    private volatile  static SingleTon instance = null;
    private SingleTon(){

    }

    public static SingleTon getInstance(){
        if(instance == null){                   //1
            synchronized (SingleTon.class){
                if(instance == null){
                    instance = new SingleTon(); //2
                }
            }
        }
        return instance;
    }
}
复制代码

面试官:“小伙子,字写的不错嘛,能说下为什么要if判断两次吗?”

第一次校验是为了提高程序的效率,避免每次都要加锁获取对象。 第二次校验是为了避免并发问题,避免线程A走到第一个判断时,线程B也走到了这里,之后会创建2次对象。

面试官:“嗯,回答的不错,那为什么这个对象要用volatile修饰呢?”

嘿嘿,果不其然,这是单例必问的问题,还好我早有准备。

因为volatile可以保证可见性和有序性

new SingleTon()这段代码,经过javac反编译分析后,会有如下几个步骤:

  • 给 instance 分配内存
  • 调用 Singleton 的构造函数来初始化成员变量
  • 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

上面的代码在编译器运行时,可能会出现重排序 从1-2-3 排序为1-3-2

如此在多线程下就会出现问题

例如现在有2个线程A,B

线程A在执行第5行代码时,B线程进来,而此时A执行了 1和3,没有执行2,此时B线程判断s不为null 直接返回一个未初始化的对象,就会出现问题

而用了volatile,上面的重排序就会在多线程环境中禁止,不会出现上述问题。

面试官:“嗯,看来你对volatile有不错的理解,那你能说说为什么volatile可以保证可见性和有序性吗?”

我擦,真的要这么深入吗?不应该是九浅一深吗?这还不到9次就深啦?我还没准备好呢,会疼的呀。而且这个我真不会啊。。早知道就提前看看《大王叫下》的文章了,唉。

额,我还没准备好,能不能下次再回答。

面试官微微一笑:“嗯,没事。再问个问题,这样的单例模式是真正安全的吗?”

当然是线程安全的啦

面试官:“那除了线程,其他方面是安全的吗?”

啥?其他方面?还有哪方面?我带TT了呀,呸呸,想哪去了。

这个额。。我不是很了解

面试官:“嗯,没事,回去可以了解一下,今天的面试就先到这里吧”

好的,那我回去准 ??? 啥情况,这就结束了?我裤子都脱了,你给我看这个?

我还有精心准备的中日双语自我介绍没说呢。

啥玩意啊,面试体验极差,哼,你们会为失去我这样的人才后悔的!

面试官:“我马上还有个会议,回头再联系你”

哦哦哦。好的,谢谢面试官哈,我再沉淀沉淀,争取早日加入组织。

唉,真的是,这也太快了吧,毫无体验,还没开始就结束了。 不过面试官说的其他方面安全是是那么意思呢?

PS : 以上内容,纯属虚构,如有雷同,概不负责。

那我们就来一起研究下吧,代码如下:

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条指令:

  • 1、根据类型分配一块内存区域
  • 2、把第一条指令返回的内存地址压入操作数栈顶
  • 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,性能好;简单
  • 缺点:无法避免被反射、反序列化破坏

2、枚举

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 这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

3、ThreadLocal单例模式

public class Singleton {
    
    private Singleton(){}
    
    private static final ThreadLocal threadLocal = 
            new ThreadLocal(){
                @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> LOOKUP_KEY_HOLDER = new ThreadLocal() {
        protected Object initialValue() {
            return new LinkedBlockingDeque();
        }
	private DynamicDataSourceContextHolder() {
    }

    public static String getDataSourceLookupKey() {
        LinkedBlockingDeque 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 deque = (LinkedBlockingDeque)LOOKUP_KEY_HOLDER.get();
        if (deque.isEmpty()) {
            LOOKUP_KEY_HOLDER.remove();
        } else {
            deque.pollFirst();
        }
    }
    };
}
复制代码

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

最后

原创不易,请好好珍惜疼爱我哦,多多点赞关注,给我更大的动力。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Dubbo源码学习-服务发布

    我们一般会把服务的信息放在spring的配置文件中,供dubbo解析调用。那么这些配置文件是怎么起作用的呢?

    大王叫下
  • Spring Ioc源码分析 之 Bean的加载(五):实例化Bean

    在doCreateBean()代码 <2> 处,有一行代码instanceWrapper = createBeanInstance(beanName, mbd,...

    大王叫下
  • SpringAop源码分析(基于注解)四:拦截器链

    本文依据JdkDynamicAopProxy来分析,对CGLIB感兴趣的同学看一看ObjenesisCglibAopProxy相关代码。 JdkDynamicA...

    大王叫下
  • 三、单例模式详解

    2、单例模式是非常经典的高频面试题,希望通过面试单例彰显技术深度,顺利拿到Offer的人群。

    编程之心
  • 设计模式-门面模式

    cwl_java
  • SpringBoot定时任务

    使用 Spring 自带的定时任务处理器 @Scheduled 注解 使用:添加 web 依赖 spring-boot-starter-web

    关忆北.
  • 逐行阅读Spring5.X源码(七)扫描和注册神器 ConfigurationClassPostProcessor ,学此类者,胜过学九阳神功!胆小勿入!

    ConfigurationClassPostProcessor是一个BeanFactory的后置处理器,因此它的主要功能是参与BeanFacto...

    源码之路
  • Java多线程:捕获线程异常

    喜欢天文的pony站长
  • HashSet内部原理解析Header源码解析Footer

    俞其荣
  • 10(01)总结形式参数,包,修饰符,内部类

    类,抽象类,接口的综合小练习 /* 教练和运动员案例(学生分析然后讲解) 乒乓球运动员和篮球运动员。 乒乓球教练和篮球教练。 为了出国交流,跟乒乓球相关...

    Java帮帮

扫码关注云+社区

领取腾讯云代金券