这个这个。。。本王最近由于开始找实习工作了,所以就在牛客网上刷一些公司的面试题,大多都是一些java,前端HTML,js,jquery,以及一些好久没有碰的算法题,说实话,有点难受,其实在我不知道的很多是地方还有很多很多的知识漏洞,就像这一次写的这个,也是我在刷题的时候感觉到真的是我空缺的地方,为什么呢?因为,做多了,错多了。然而很尴尬的又是因为这个只是也是很多公司的面试题,所以索性直接写下来整理一遍。

在这里我也建议各位,牛客网不仅仅是一个找工作的station也是一个可以锻炼我们的地方,没事刷刷题啊,逛逛论坛啊,说不定就能找到很多你意想不到的东西。

在面试的过程中,有几个问题是比较常见的。

* HashTable、HashMap、ConcurrentHashMap的区别?
* HashMap线程不安全的出现场景?
* HashMap put方法存放数据时是怎么判断是否重复的?   
* JDK7和JDK8 中HashMap的实现有什么区别?
* HashMap的长度为什么是2的幂次方?
只要把这几个问题过一遍之后,大致了解了他们各自的作用与互相之间的区别再!!去敲一遍其实就可以掌握了。

HashTable

* 底层数组+链表实现,无论key还是value都不能为null,线程安全
,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
* 初始size为11,扩容:newsize = oldsize*2+1
* 计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
HashMap

* 在底层数组+链表中实现,线程不安全,可存储null键和null值
* 初始size为16,可扩容:newsize = oldsize*2,size一定为2的n次幂
* 扩容针对整个Map,每次扩容的时候,原来数组中的元素依次重新计算存放的位置,并重新插入
* 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
* 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
* 计算index方法:index = hash & (tab.length – 1)
*HashMap的初始值还要考虑加载因子:

哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。

加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。
空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。

HashMap与HashTable的区别(面试题常考~)

1.两者所继承的父类不同



HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。

在这里原本是截取了JDK API1.6 中文版里面的,但实在是太丑了,就在别人的博客,呵呵,悄咪咪的拿了过来借鉴了一下





 2.两者对外接口是不同的

HashTable比HashMap多提供了elements()和contains()两个方法。

elements()方法继承自HashTable的父类Doctionnary。elements()方法用于返回此时HashTable中的值的枚举。



contains()方法判断该Hashtable是否包含传入的value。它的作用与containsValue()一致。事实上,contansValue()
就只是调用了一下contains() 方法,是判断哈希表中是否包含指定的值。如图是contains的源码:
public virtual bool Contains(object key) { return this.ContainsKey(key); }
3.对Null key 和Null value的支持不同
Hashtable既不支持Null key也不支持Null value。


HashMap中,key-value都是存在Entry中的。null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null,不保证元素的顺序恒久不变,它的底层使用的是数组和链表,用过HashCode()方法和equal()方法来保证键的唯一性。当get()方法返回null值时,可能是
HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键,
而应该用containsKey()方法来判断。
4.线程安全的不同性


Hashtable是线程安全的,它的每个方法中都加入了Synchronize方法。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步。

HashMap不是线程安全的,在多线程并发的环境下,可能会产生死锁等问题。所以使用HashMap时就必须要自己增加同步处理,


虽然HashMap不是线程安全的,但是它的效率会比Hashtable要好很多。这样设计是合理的。在我们的日常使用当中,大部分时间是单线程操作的。HashMap把这部分操作解放出来了。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
5.Hash值的计算方法不同

为了求得元素的位置,需要根据元素的Key计算出一个哈希值,然后再用这个哈希值来计算出崔忠的位置。




Hashtable直接使用对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数发来获得最终的位置。

Hashtable在计算元素的位置时需要进行一次除法运算,而除法运算是比较耗时的。

HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。

HashMap的效率虽然提高了,但是hash冲突却也增加了。因为它得出的hash值的低位相同的概率比较高,而计算位运算


为了解决这个问题,HashMap重新根据hashcode计算hash值后,又对hash值做了一些运算来打散数据。使得取得的位置更加分散,从而减少了hash冲突。当然了,为了高效,HashMap只做了一些简单的位处理。从而不至于把使用2
的幂次方带来的效率提升给抵消掉。

ConcurrentHashMap

* 底层采用分段的数组+链表实现,线程安全。
* key和value都不能为null。
* 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。
*
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
*
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
* 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
这个就很棒了,上面总结的源于某猿大神,Java5提供的ConcurrentHashMap就像是HashTable的升级版,扩容性更强。


在HashMap中,通过get()返回的null值,既可以表示返回该Key所对应过的Value是null值,也可以表示为没有该Key,在这种情况下就应该采用ConcurrentHashMap。

来看一张简单的类图:




ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一个可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素。当对HashEntry数组的数据进行修改时,必须首先获得与它对应的segment锁。


 Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。


简单理解就是,ConcurrentHashMap是一个Segment数组,Segment通过继承ReentrantLock来进行加锁,所以每次需要加锁的操作锁住的是一个Segment,只要保证每个Segment是线程安全的,也就实现了全局的线程安全。
重申一下,Segment数组不能扩容,扩容是Segment数组某个位置内部的数组HashEntry<K,V>[]进行扩容,扩容后,容量为原来的2倍。
可以回顾下出发扩容的地方,put的时候,如果判断该值的插入会导致该Segment的元素个数超过阈值,那么先进行扩容,再插值。

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信