CurrentHashMap原理

283 阅读3分钟

我们带着问题去探究CurrentHashMap的原理:

image.png

CurrentHashMap实现原理是什么

CurrentHashMap在JDK1.7和JDK1.8实现方式是不同的

JDK1.7

JDK1.7 中的 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。

如下图所示,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发。 image.png

Segment继承了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色。Segment默认为16,也就是并发度为16.

存放元素的HashEntry,也是一个静态内部类,主要成员如下:

image.png

其中,用volatile修饰了HashEntry的数据value和下一个节点next,保证了多线程环境下数据获取的可见性

JDK1.8

在数据结构上,JDK1.8采用了CurrentHashMap选择了与HashMap相同的Node+链表+红黑树书结构;在锁的实现上,抛弃了原有的Segment分段锁,采用CAS+synchronized实现更加细粒度的锁。

将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。

image.png

JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?★★★★★

在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。 减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。

ConcurrentHashMap 的 put 方法执行逻辑是什么?★★★★

  1. 大致可以分为以下步骤:
  2. 根据 key 计算出 hash 值;
  3. 判断是否需要进行初始化;
  4. 定位到 Node,拿到首节点 f,判断首节点 f:
  5. 如果为 null ,则通过 CAS 的方式尝试添加;
  6. 如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容;
  7. 如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入;

CurrentHashMap的并发度是多少?★★★★

并发度可以理解为程序运行时能够同时更新 ConccurentHashMap且不产生锁竞争的最大线程数。在JDK1.7中,实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度,默认是16,这个值可以在构造函数中设置。

如果自己设置了并发度,ConcurrentHashMap 会使用大于等于该值的最小的2的幂指数作为实际并发度,也就是比如你设置的值是17,那么实际并发度是32。

如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。

在JDK1.8中,已经摒弃了Segment的概念,选择了Node数组+链表+红黑树结构,并发度大小依赖于数组的大小。

ConcurrentHashMap 迭代器是强一致性还是弱一致性?★★

与 HashMap 迭代器是强一致性不同,ConcurrentHashMap 迭代器是弱一致性。 ConcurrentHashMap 的迭代器创建后,就会按照哈希表结构遍历每个元素,但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反映出来,而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来,这就是弱一致性。