HashMap、HashTable及ConcurrentHashMap区别及相关面试题

前言


第一次写博客,水平有限可能有理解不到位或理解错的地方。欢迎各位大神参与讨论或指正。
Map在工作中的使用频率较高,HashMap相关的问题在面试中也经常被问到。所以抽空在网上找资料对它们进行了系统的学习,作出以下几点总结:

  1. HashMap、HashTable及ConcurrentHashMap的区别。
  2. HashMap的工作原理。
  3. 相关面试题。

一、HashMap、HashTable及ConcurrentHashMap的区别

1. 底层实现/容纳数据

  • hashMap:底层数组+链表实现;key和value都可为null。初始容量为16。扩容:newsize = oldsize*2,size一定为2的n次幂。
  • hashTable:底层数组+链表实现;key和value都不能为null。初始容量为11。扩容:newsize = olesize*2+1。
  • ConcurrentHashMap:底层采用分段的数组+链表实现。初始容量为16。扩容:段内扩容,不会对整个Map扩容。

2. 线程安全与效率

  • hashMap:线程不安全,效率相对较高。多线程下扩容resize可能出现死循环现象。
  • hashTable:线程安全。实现线程安全的方式是在修改数据时锁住整个HashTable,效率低。(原因:synchronized是针对整张Hash表的,即每次锁住整张表让线程独占)
  • ConcurrentHashMap: 线程安全。通过把整个Map分为N个Segment,可以提供相同的线程安全,同时使效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)

3. 迭代器(Iterator)

  • hashMap:HashMap的迭代器(Iterator)是fail-fast迭代器。当其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。
  • hashTable:Hashtable的enumerator迭代器不是fail-fast的。
  • ConcurrentHashMap: 弱一致迭代器。

二、HashMap的工作原理

1. 底层思想
HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。从而实现对数据的读写。

2. 工作原理

  • 键值对传递给put()方法时,调用键对象的hashCode()方法来计算hashcode;
  • 通过hashcode找到bucket位置来存储Entry(值)对象;
  • 获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。
  • HashMap使用链表来解决碰撞问题,当发生碰撞时,对象将会储存在链表的下一个节点中。
  • HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD,8),链表就会被改造为树形结构。(原因:链表太长效率低)

三、相关面试题

1.当两个对象的hashcode相同会发生什么?

因为hashcode相同,所以它们的bucket位置相同,会发生‘碰撞’。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。

2.如果两个键的hashcode相同,你如何获取值对象?

当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。如果两个值对象hashcode相同存储在同一个bucket,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

3.HashMap的初始值该如何计算?

HashMap的初始值根据负载因子计算。为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时(负载因子默认0.75),即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。所以此时应给256最为合适。(容量给2的N次方,原因在这儿就不详细说了,大家可以网上搜索扩展一下这个知识)

4.HashTable和ConcurrentHashMap都是线程安全的,为什么ConcurrentHashMap效率要高?

Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。
注意:有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。

5.如何让HashMap同步?

 Map m = Collections.synchronizeMap(hashMap);

6.如何减少/降低Hash碰撞?

使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。

7.如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

8.重新调整HashMap大小会存在什么问题?

多线程的情况下,可能产生条件竞争(race condition)。
当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。这个时候,你可以质问面试官,为什么这么奇怪,要在多线程的环境下使用HashMap呢?

9.为什么String, Interger这样的wrapper类适合作为键?

String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

10.我们可以使用自定义的对象作为键吗?

这是前一个问题的延伸。当然你可能使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。

四、结构图

Map家族结构图

五、尊重原创

面试必备:HashMap、Hashtable、ConcurrentHashMap的原理与区别
HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别
谈谈HashMap线程不安全的体现

感谢阅读。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值