面试连环炮:从HashSet开始,一路怼到CPU
之前在金三银四,我面试了20家互联网公司这篇文章中提到我在今年四月份面试了大大小小近20家互联网公司,然后总结出来一套由浅入深的Java基础面试题连环炮。本文主要给出这些面试题的答案。
由浅入深了解这些面试题背后的知识基本能hold住80%的Java面试题,在此基础上再去学习拓展其他Java相关知识点,核心是把自己所掌握的零零散散的知识点串联起来,形成自己的知识体系。
系列面试题如下:
接下来我们就给出每一题的答案,希望大家在这个过程中能够有所贯通。
本文所有内容都是基于Jdk1.8版本。建议大家收藏后持续阅读!关注“java架构设计”阅读更多技术文章~
HashSet的底层是用什么来实现的?
HashSet一定是大家常用的一种数据结构,但是你在天天用的时候,有没有想过它的底层到底是怎么实现的呢?有没有点击进去看看JDK的源码呢?
如果大家英文阅读能力ok,看源码的时候一定要看看类注释(英文不好的可以借助翻译工具)!比如打开java.util.HashSet类源码,类注释第一段是这样的:
This class implements the <tt>Set</tt> interface,
backed by a hash table (actually a <tt>HashMap</tt> instance).
It makes no guarantees as to the iteration order of the set;
in particular, it does not guarantee that the order will remain constant over time.
This class permits the <tt>null</tt> element.
翻译过来就是:
这个类实现了由哈希表支持的Set接口
(实际上是一个HashMap实例)。它不能保证
集合的迭代顺序;特别是,它不能保证
随着时间的推移,秩序将保持不变。这个类允许null元素。
再看它的各种构造方法:
就能知道它的底层实现就是一个HashMap!
那么HashSet的底层是用什么来实现的是不是就很清晰了,问题到此结束,不再拓展,感兴趣的同学可以继续看源码。
对HashSet集合装入的对象有什么要求?
这道题很简单,但是也很绕。尤其是我们在面试的时候,紧张的状态下很多时候头脑是不够清晰的,那么就容易get不到这个点。
我们经常会用一个HashSet来装入一组数据,比如这样:
Set<User> users = new HashSet<User>(10);
User user = new User();users.add(user);
问的就是这个User对象有什么要求?首先我们要了解HashSet集合的特性:
- 无序的
- 不可重复的
为什么是无序的?第一题已经说到了,他用的是HashMap,其实就是HashMap里面的key,那自然就是无序的了。
如何保证不可重复的?换句话说,我怎么判定一个对象在集合里面是否已经存在了?
相信大家会立马就知道一个对象是否相等需要根据hashCode()和equals()方法来判断,这也就是为什么说一个对象要重写它的hashCode()方法和equals()方法了。
比如java.lang.String类一定有它自己的hashCode()和equals()方法。
那么这题的答案就是:一定要重写它的hashCode()和equals()方法!
get到这个点,相信大家对hashCode()和equals()方法的相关问题一定是手到擒来,了如指掌了吧?
HashMap的实现原理?
大家有没有发现,上面说来说去说的其实还是HashMap,这也就是说为什么是面试连环炮,你能聊下去,自然就聊到HashMap了。
有的同学说我就不按照你的套路来,就聊不下去?那你觉得这次面试还有戏吗?
所以这个套路是跑不掉的,不聊HashMap我怎么和你聊多线程?怎么和你聊安全的Map?
所以,不管怎么学,大家一定要把HashMap的底层原理、jdk源码烂熟于心,把底子打好!这样才有资格去继续学习后面的ConcurrentHashMap、synchronized、CAS、Lock、ThreadLocal等等。
关于HashMap的底层实现原理,这里不赘述,大家可以直接看我的另外一篇文章:
了解HashMap原理后再回头看这些问题:
HashMap中计算元素的下标公式为什么是hash(key) & (size - 1)?
这里面有几个知识点:
- hash(Object key)方法的实现
- 和hashMap的size-1做与运算
- 为什么是size-1?
- HashMap的容量为什么都是2的n次方?
简单来说第一个知识点就是为了得到的hashCode更加的散列:key的hashCode值和高16位做异或运算,来使得低位的值更加的散列。
更加散列的目的就是为了和size-1做与运算的时候分布更均匀!
为什么是size-1?为什么容量是2的n次方?
也就是HashMap的容量只会是4、8、16、32这样2倍扩容,那么size-1就是对应的3、7、15、31,换算成二进制便是:11、111、1111、11111,所以在做与运算的时候,一定就是取hash(Object key)的低几位!
HashMap是线程安全的吗?
当然不是!是的话还有后面的HashTable、ConcurrentHashMap什么事情?
HashMap不是线程安全的,他的get、put操作都没有加锁,意味着多线程环境下,无法保证put进去的值,get之后还是这个值。
那你知道哪些线程安全的Map吗?
- Collections.synchronizedMap(Map<k, v> map)
- HashTable
- ConcurrentHashMap
它们都是怎么来保证线程安全的?
- Collections有一个内部静态类SynchronizedMap,它里面有一个Map和mutex排斥锁。这个时候再去操作map的时候,相关方法全部加上了synchronize(mutex)。
- HashTable是线程安全的,他对数据操作的时候也就是put时候都会上锁。
- 在1.7中,ConcurrentHashMap有一个内部类Segment,他继承了ReentranLock可重入锁,Segment有点类似于HashMap,它里面有一个HashEntry数组,整体结构还是数组+链表,但是这个数组是用volatile实现的。在1.8之后就不是了,而是采用CAS+Synchronized来保证并发安全性。
复杂的ConcurrentHashMap#put操作
synchronized、锁升级、CAS、Lock锁
通过上面的HashMap、ConcurrentHashMap自然就引入到synchronized关键字了。
以前都说它慢,是个重量级锁,需要向操作系统申请mutex锁资源,为什么现在一直在用呢?
这个时候就得去了解锁升级了,jdk1.6版本以后对sychronized的优化就是锁升级的过程,大多数情况下,无锁即可满足我们的业务场景。只有在极其苛刻的条件下才会升级为重量级锁。
那为什么重量级锁会慢呢?这就怼到CPU层了,应用程序会向操作系统排队申请Mutex锁资源。
这又涉及到用户态和内核态的切换了。。。
锁升级的过程大家可以专门找一篇文章去详细了解,也是大厂面试装B必问,这里不赘述。
同理锁升级的过程中就是CAS自旋锁的步骤,所以CAS也得必知必会呀。。。
还有上面提到的1.7版本中ConcurrentHashMap里面使用到的ReentrantLock可重入锁,每一块进入都是一个全新的世界!
而需要我们做的就是从线头开始一点一点的梳理、挖掘出这些知识点,形成我们自己所独有的技术线。