面试官:请你说说HashMap、ConcurrentHashMap的原理和实现

1,800 阅读4分钟

概念

✔在多线程高并发环境下,使用HashMap进行put、remove操作时存在数据丢失的情况,也就是说HashMap是非线程安全的,因为HashMap底层代码操作数组的数据并未加锁,为了避免这个隐患,强烈推荐使用ConcurrentHashMap代替HashMap

HashTable是一个线程安全类,实际上它底层使用synchronized来锁住整张hash表来实现线程安全,即每次锁住整张表让线程独占,相当一所有的线程进行读写时都去竞争同一把锁,导致效率非常低下,而ConcurrentHashMap可以做到读取数据的时候不加锁,并且其内部结果可以让其在进行写操作的时候能把锁的颗粒度保持尽量最小,允许多个修改操作同时进行,其关键于使用了分段锁机制,它使用了多个锁来控制对hash表的不同部分就那些修改,对于JDK1.7版本的实现,ConcurrentHashMap内部使用段(Segemnt)来说表示不同的16个部分,也就是一个 Segment 数组,一个 Segment 数组里面包含 HashEntry 数组,当对某个 HashEntry数组中的元素进行修改时,必须首先获得该元素所属HashEntry数组对应的Segment锁,每个Segment都有自己独立的锁,只要多个修改操作发生在不同段上,它们就可以并行执行,👏JDK1.8的实现降低了锁的颗粒度,不同于JDK1.7版本的Segemnt数组+HashEntry链表,JDK1.8版本中的ConcurrentHashMap直接抛弃了Segment锁,一个ConcurrentHashMap包含一个Node数组(和HashEntry实现差不多),每个Node是一个链表结构,并且在链表长度大于一定值时会自动转换为红黑树结构(TreeBin)。

🌈JDK1.7和JDK1.8对比

HashMap

JDK 1.7

  • 数据结构:数组+链表
  • 安全性:非线程安全,操作数组的数据并未加锁。

JDK 1.8

  • 数据结构:数组+链表+红黑树
  • 安全性:非线程安全,操作数组的数据并未加锁。

🔥为了减少链表遍历的开销,Java 8对HashMa进行了优化,将数据结构改成数组+链表或红黑树,在链表中当元素长度>=8时,HashMap会将链表结构转换成红黑树结构以提高查询效率,哈希表扩容时,如果发现链表长度<=6的时候,则会将红黑树退化成链表。

ConcurrentHashMap

JDK1.7

  • 数据结构:Segment 分段数组 + HashEntry 数组
  • 线程安全:线程安全,因为底层代码在操作每一个Segment时都会加锁,保证线程安全
  • 性能:读取数据不加锁、效,且因为map中的value变量值是添加volatile关键字修饰,可保证读取到最新值,降低CPU负载。

JDK1.8

  • 数据结构:Node数组 + 链表 + 红黑树(TreeBin)
  • 线程安全:线程安全,并发操作使用的是synchronizedCAS来控制,引入了红黑树结构降低哈希冲突和时间复杂度。

JDK1.7当中Segment数组的意义就是将一个大的hashTable分割成多个小的hashTable来进行加锁,减低的锁的粒度,也就是所说的锁分离技术,每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构是一致的。

JDK 1.7中的put/get操作
put操作:对于ConcurrentHashMap的数据插入,这里需要进行两次的hash计算index值,第一次是计算当前key存在当哪一块小的segment中,第二次hash操作找相应的HashEntry位置。
get操作:对于数据获取,ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。

JDK1.7 HashMap和ConcurrentHashMap示意图HashMap和ConcurrentHashMap区别.png

JDK1.7 HashMap的数据结构图如上所示,其内部实际上是一个数组,数组中的每个元素都是一个单向链表,链表中的每个元素都是嵌套类Entry的实例,Entry实例包含了4个属性:key、value、hash值和用于指向单向链表下一个元素的next节点。

JDK1.7ConcurrentHashMap数据结构.png

JDK1.8ConcurrentHashMap数据结构.png

🙋‍♂️与HashMap不同,ConcurrentHashMap采用分段锁的思想实现并发操作,因此是线程安全的。ConcurrentHashMap是由多个Segment组成(Segment的数量也就是锁的并发度),每个Segment都是继承自ReentrantLock并单独加锁,索引每次进行加锁操作锁住的都是一个Segemnt,这样保证了每个Segment都是线程安全的,进而实现了全局的线程安全。

🎈ConcurrentHashMap在JDK 7和8之间的区别

  • JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)。
  • JDK1.8ConcurrentHashMap 取消了 Segment 分段锁,采用CASsynchronized 来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))。
  • JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档。