面试处处都是坑啊?让实现线程安全的单例,又不让用synchronized

单例模式,是Java中比较常见的一个设计模式,也是我在面试时经常会问到的一个问题。

经过我的初步统计,基本上有60%左右的人可以说出2-4种单例的实现方式,有40%左右的人可以说出5-6种单例的实现方式,只有20%左右的人能够说出7种单例的实现。

而只有不到1%的人能够说出7种以上的单例实现。

其实,作为面试官,我大多数情况下之所以问单例模式,是因为这个题目可以问到很多知识点。

比如线程安全、类加载机制、synchronized的原理、volatile的原理、指令重排与内存屏障、枚举的实现、反射与单例模式、序列化如何破坏单例、CAS、CAS的ABA问题、Threadlocal等知识。

一般情况下,只需要从单例开始问起,大概就可以完成一场面试的整个流程,把我想问的东西都问完,可以比较全面的了解一个面试者的水平。

以下,是一次面试现场的还原,从单例模式开始:

Q:你知道怎么不使用synchronized和lock实现一个线程安全的单例吗?

A:我知道,可以使用"静态内部类"实现。

静态内部类实现单例模式:

Q:除了静态内部类还会其他的方式吗?

A:还有就是两种饿汉模式。

饿汉实现单例模式:

饿汉变种实现单例模式:

Q:那你上面提到的几种都是线程安全的吗?

A:是线程安全的

Q:那是如何做到线程安全的呢?

A:应该是因为我使用了static,然后类加载的时候就线程安全了吧?

Q:其实你说的并不完全对,因为以上几种虽然没有直接使用synchronized,但是也是间接用到了。

(这里面根据回答情况会朝两个不同的方向展开:1、类加载机制、模块化等;2、继续深入问单例模式)

类加载过程的线程安全性保证

以上的静态内部类、饿汉等模式均是通过定义静态的成员变量,以保证单例对象可以在类初始化的过程中被实例化。

这其实是利用了ClassLoader的线程安全机制。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。

所以, 除非被重写,这个方法默认在整个装载过程中都是线程安全的。所以在类加载过程中对象的创建也是线程安全的。

Q:那还回到刚开始的问题,你知道怎么不使用synchronized和lock实现一个线程安全的单例吗?

(并不是故意穷追不舍,而是希望能可以引发面试者的更多思考)

A:额、、、那枚举吧,枚举也可以实现单例。

枚举实现单例模式:

Q:那你知道枚举单例的原理吗?如何保证线程安全的呢?

枚举单例的线程安全问题

枚举其实底层是依赖Enum类实现的,这个类的成员变量都是static类型的,并且在静态代码块中实例化的,和饿汉有点像, 所以他天然是线程安全的。

Q:所以,枚举其实也是借助了synchronized的,那你知道哪种方式可以完全不使用synchronized的吗?

A:en....我想想

Q:(过了一会他好像没有思路)你知道CAS吗?使用CAS可以实现单例吗?

(面试中,如果面试者对于锁比较了解的话,那我大多数情况下都会继续朝两个方向深入问:1、锁的实现原理;2、非锁,如CAS、ThreadLocal等)

A:哦,我知道,CAS是一项乐观锁技术,当多个线程尝试使用CAS同时更新一个变量时,只有其中一个线程能更新成功。

借助CAS(AtomicReference)实现单例模式:

Q:使用CAS实现的单例有没有什么优缺点呀?

A:用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。

Q:你说的好像是优点?那缺点呢?

CAS实现的单例的缺点

CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。

另外,代码中,如果N个线程同时执行到 singleton = new Singleton();的时候,会有大量对象被创建,可能导致内存溢出。

Q:好的,除了使用CAS以外,你还知道有什么办法可以不使用synchronized实现单例吗?

A:这回真的不太知道了。

Q:(那我再提醒他一下吧)可以考虑下ThreadLocal,看看能不能实现?

(面试者没有思路的时候,我几乎都会先做一下提醒,实在没有思路再换下一个问题)

A:ThreadLocal?这也可以吗?

Q:你先说下你理解的ThreadLocal是什么吧

(通过他的回答,貌似对这个思路有些疑惑,不着急。先问一个简单的问题,让面试者放松一下,找找自信,然后再继续问)

ThreadLoacal

ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制(synchronized)采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。

同步机制仅提供一份变量,让不同的线程排队访问,而ThreadLocal为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

Q:那理论上是不是可以使用ThreadLocal来实现单例呢?

A:应该也是可行的。

使用ThreadLocal实现单例模式:

Q:嗯嗯,好的,那有关单例模式的实现的问题我就问的差不多了。

(ThreadLocal这种写法主要是考察面试者对于ThreadLocal的理解,以及是否可以把知识活学活用,但是实际上,这种所谓的"单例",其实失去了单例的意义...)

(但是说实话,能回答到这一题的人很少,大多数面试者基本上在前面几道题就已经没有思路了,大多数情况下根本不会问到这个问题就要改方向了)

A:(心中窃喜)嗯嗯,学习到很多,感谢

Q:那...你知道如何破坏单例吗?

(单例问题,必问的一个。通过这个引申到序列化和反射的相关知识)

A:(额....)

内心一万头神兽胡腾而过,真想站起来手撕面试官啊,但是还要平心静气的回答

破坏单列模式的方式

  1. 反射
  2. 序列和反序列化

这里我以静态内部类单例来举例,先看下静态内部类单例的代码

 
public class LazyInnerClassSingleton { // 私有的构造方法 private LazyInnerClassSingleton(){} // 公有的获取实例方法 public static final LazyInnerClassSingleton getInstance(){ return LazyHolder.LAZY; } // 静态内部类 private static class LazyHolder{ private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } }

03、反射破坏单例模式

我们来看代码

 
public static void main(String[] args) { try { //很无聊的情况下,进行破坏 Class<?> clazz = LazyInnerClassSingleton.class; //通过反射拿到私有的构造方法 Constructor c = clazz.getDeclaredConstructor(null); //因为要访问私有的构造方法,这里要设为true,相当于让你有权限去操作 c.setAccessible(true); //暴力初始化 Object o1 = c.newInstance(); //调用了两次构造方法,相当于 new 了两次 Object o2 = c.newInstance(); //这里输出结果为false System.out.println(o1 == o2); } catch (Exception e) { e.printStackTrace(); } }

输出为false,说明内存地址不同,就是实例化了多次,破坏了单例模式的特性。

04、防止反射破坏单例模式

通过上面反射破坏单例模式的代码,我们可以知道,反射也是通过调用构造方法来实例化对象,那么我们可以在构造函数里面做点事情来防止反射,我们把静态内部类单例的代码改造一下,看代码

 
public class LazyInnerClassSingleton { // 私有的构造方法 private LazyInnerClassSingleton(){ // 防止反射创建多个对象 if(LazyHolder.LAZY != null){ throw new RuntimeException("不允许创建多个实例"); } } // 公有的获取实例方法 public static final LazyInnerClassSingleton getInstance(){ return LazyHolder.LAZY; } // 静态内部类 private static class LazyHolder{ private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } }

这样我们在通过反射创建单例对象的时候,多次创建就会抛出异常

 
java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.cl.singleton.LazySingletonTest.main(LazySingletonTest.java:68) Caused by: java.lang.RuntimeException: 只能实例化1个对象 at com.cl.singleton.LazyInnerClassSingleton.<init>(LazyInnerClassSingleton.java:18) ... 5 more

序列化破坏单例模式

用序列化的方式,需要在静态内部类(LazyInnerClassSingleton) 实现 Serializable 接口,代码在下面的防止序列化破坏单例模式里面

这里我们先来看下序列和反序列的代码

 
//序列化创建单例类 public static void main(String[] args) { LazyInnerClassSingleton s1 = null; //通过类本身获得实例对象 LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance(); FileOutputStream fos = null; try { //序列化到文件中 fos = new FileOutputStream("SeriableSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s2); oos.flush(); oos.close(); //从文件中反序列化为对象 FileInputStream fis = new FileInputStream("SeriableSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (LazyInnerClassSingleton) ois.readObject(); ois.close(); //对比结果,这里输出的结果为false System.out.println(s1 == s2); } catch (Exception e) { e.printStackTrace(); } }

结果为false,说明也破坏了单例模式

06、防止序列化破坏单例模式

这里我们先来看下改造后的代码,然后分析原理

 
public class LazyInnerClassSingleton implements Serializable { private static final long serialVersionUID = -4264591697494981165L; // 私有的构造方法 private LazyInnerClassSingleton(){ // 防止反射创建多个对象 if(LazyHolder.LAZY != null){ throw new RuntimeException("只能实例化1个对象"); } } // 公有的获取实例方法 public static final LazyInnerClassSingleton getInstance(){ return LazyHolder.LAZY; } // 静态内部类 private static class LazyHolder{ private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } // 防止序列化创建多个对象,这个方法是关键 private Object readResolve(){ return LazyHolder.LAZY; } }

在执行上面序列和反序列化代码,输出true,是不是一脸懵逼,为什么加了一个readResolve方法,就能防止序列化破坏单例模式,下面就带着大家来看下序列化的源码:

 
public final Object readObject()throws IOException, ClassNotFoundException{ if (enableOverride) { return readObjectOverride(); } // if nested read, passHandle contains handle of enclosing object int outerHandle = passHandle; try { //看这里,看这里,就是我readObject0 Object obj = readObject0(false); handles.markDependency(outerHandle, passHandle); ClassNotFoundException ex = handles.lookupException(passHandle); if (ex != null) { throw ex; } if (depth == 0) { vlist.doCallbacks(); } return obj; } finally { passHandle = outerHandle; if (closed && depth == 0) { clear(); } } }

然后我们看下 readObject0 这个方法

 
private Object readObject0(boolean unshared) throws IOException { ... //主要是这个判断 case TC_OBJECT: //然后进入readOrdinaryObject这个方法 return checkResolve(readOrdinaryObject(unshared)); ... }

然后我们看下readOrdinaryObject 这个方法

 
private Object readOrdinaryObject(boolean unshared)throws IOException{ ... Object obj; try { //这里判断是否有无参的构造函数,有的话就调用newInstance()实例化对象 obj = desc.isInstantiable() ? desc.newInstance() : null; ... if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); ... }

这里的关键是desc.hasReadResolveMethod() ,这段代码的意思是查看你的单例类里面有没有readResolve方法,有的话就利用反射的方式执行这个方法,具体是desc.invokeReadResolve(obj)这段代码,返回单例对象。这里其实是实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,这也算是一个缺点吧。

Q:好了,你可以去跟人事交流一下入职的手续了

至此,面试结束,哈哈哈哈,offer到手,简单吗?不简单,难嘛?不难

但是真的需要平时的积累和学习,这样才能针对面试官问题,回答出让他无可挑剔的答案

欢迎关注公众号:Java架构师联盟,每日更细技术好文

原创文章 134 获赞 66 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_42864905/article/details/106062377