ConcurrentHashMap在Java7和中的异同点是怎样的

63次阅读
没有评论

共计 4480 个字符,预计需要花费 12 分钟才能阅读完成。

本篇文章为大家展示了 ConcurrentHashMap 在 Java7 和中的异同点是怎样的,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

在 Java 8 中,对于 ConcurrentHashMap 这个常用的工具类进行了很大的升级,对比之前 Java 7 版本在诸多方面都进行了调整和变化。

一:Java 7 版本的 ConcurrentHashMap

从图中我们可以看出,在 ConcurrentHashMap 内部进行了 Segment 分段,Segment 继承了 ReentrantLock,可以理解为一把锁,各个 Segment 之间都是相互独立上锁的,互不影响。相比于之前的 Hashtable 每次操作都需要把整个对象锁住而言,大大提高了并发效率。因为它的锁与锁之间是独立的,而不是整个对象只有一把锁。

每个 Segment 的底层数据结构与 HashMap 类似,仍然是数组和链表组成的拉链法结构。默认有 0~15 共 16 个 Segment,所以最多可以同时支持 16 个线程并发操作(操作分别分布在不同的 Segment 上)。这个默认值 16 可以在初始化的时候设置为其他值,但是一旦确认初始化以后,是不可以扩容的。

二:Java 8 版本的 ConcurrentHashMap

java 8 中,几乎完全重写了 ConcurrentHashMap,代码量从原来 Java 7 中的 1000 多行,变成了现在的 6000 多行,所以也大大提高了源码的阅读难度。

图中的节点有三种类型。

第一种是最简单的,空着的位置代表当前还没有元素来填充。

第二种就是和 HashMap 非常类似的拉链法结构,在每一个槽中会首先填入第一个节点,但是后续如果计算出相同的 Hash – 值,就用链表的形式往后进行延伸。

第三种结构就是红黑树结构,这是 Java 7 的 ConcurrentHashMap 中所没有的结构。

当第二种情况的链表长度大于某一个阈值(默认为 8),且同时满足一定的容量要求的时候,ConcurrentHashMap 便会把这个链表从链表的形式转化为红黑树的形式,目的是进一步提高它的查找性能。

由于自平衡的特点,即左右子树高度几乎一致,所以其查找性能近似于二分查找,时间复杂度是 O(log(n)) 级别;反观链表,它的时间复杂度就不一样了,如果发生了最坏的情况,可能需要遍历整个链表才能找到目标元素,时间复杂度为 O(n),远远大于红黑树的 O(log(n)),尤其是在节点越来越多的情况下,O(log(n)) 体现出的优势会更加明显。

ConcurrentHashMap 引入红黑树,好处就是避免在极端的情况下冲突链表变得很长,在查询的时候,效率会非常慢。而红黑树具有自平衡的特点,所以,即便是极端情况下,也可以保证查询效率在 O(log(n))。

2.2 Java 8 版本的 ConcurrentHashMap 的重要源码

下面我们深入源码分析。由于 Java 7 版本已经过时了,所以我们把重点放在 Java 8 版本的源码分析上。

2.1.1 Node 节点

 static class Node K,V  implements Map.Entry K,V  {
 final int hash;
 final K key;
 V value;
 Node K,V  next;
 //..........
}

每个 Node 里面是 key-value 的形式,并且把 value 用 volatile 修饰,以便保证可见性,同时内部还有一个指向下一个节点的 next 指针,方便产生链表结构。

2.1.2 put 方法源码分析

put 方法的核心是 putVal 方法:

final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) { throw new NullPointerException();
 }
 // 计算  hash  值
 int hash = spread(key.hashCode());
 int binCount = 0;
 for (Node K, V [] tab = table; ; ) {
 Node K, V  f;
 int n, i, fh;
 // 如果数组是空的,就进行初始化
 if (tab == null || (n = tab.length) == 0) { tab = initTable();
 }
 //  找该  hash  值对应的数组下标
 else if ((f = tabAt(tab, i = (n - 1)   hash)) == null) {
 // 如果该位置是空的,就用  CAS  的方式放入新值
 if (casTabAt(tab, i, null,
 new Node K, V (hash, key, value, null))) {
 break;
 }
 }
 //hash 值等于  MOVED  代表在扩容
 else if ((fh = f.hash) == MOVED) { tab = helpTransfer(tab, f);
 }
 // 槽点上是有值的情况
 else {
 V oldVal = null;
 // 用  synchronized  锁住当前槽点,保证并发安全
 synchronized (f) { if (tabAt(tab, i) == f) {
 // 如果是链表的形式
 if (fh  = 0) {
 binCount = 1;
 // 遍历链表
 for (Node K, V  e = f; ; ++binCount) {
 K ek;
 // 如果发现该  key  已存在,就判断是否需要进行覆盖,然后返回
 if (e.hash == hash  
 ((ek = e.key) == key ||
 (ek != null   key.equals(ek)))) {
 oldVal = e.val;
 if (!onlyIfAbsent) {
 e.val = value;
 }
 break;
 }
 Node K, V  pred = e;
 // 到了链表的尾部也没有发现该  key,说明之前不存在,就把新值添加到链表的最后
 if ((e = e.next) == null) {
 pred.next = new Node K, V (hash, key,
 value, null);
 break;
 }
 }
 }
 // 如果是红黑树的形式
 else if (f instanceof TreeBin) {
 Node K, V  p;
 binCount = 2;
 // 调用  putTreeVal  方法往红黑树里增加数据
 if ((p = ((TreeBin K, V) f).putTreeVal(hash, key,
 value)) != null) {
 oldVal = p.val;
 if (!onlyIfAbsent) {
 p.val = value;
 }
 }
 }
 }
 }
 if (binCount != 0) {
 // 检查是否满足条件并把链表转换为红黑树的形式,默认的  TREEIFY_THRESHOLD  阈值是  8
 if (binCount  = TREEIFY_THRESHOLD) { treeifyBin(tab, i);
 }
 //putVal  的返回是添加前的旧值,所以返回  oldVal
 if (oldVal != null) {
 return oldVal;
 }
 break;
 }
 }
 }
 addCount(1L, binCount);
 return null;
}

可以看出,方法中会逐步根据当前槽点是否初始化、空、扩容、链表、红黑树等不同情况做出不同的处理。

2.1.3 get 方法源码分析

public V get(Object key) { Node K,V [] tab; Node K,V  e, p; int n, eh; K ek;
 // 计算  hash  值
 int h = spread(key.hashCode());
 // 如果整个数组是空的,或者当前槽点的数据是空的,说明  key  对应的  value  不存在,直接返回  null
 if ((tab = table) != null   (n = tab.length)   0  
 (e = tabAt(tab, (n - 1)   h)) != null) {
 // 判断头结点是否就是我们需要的节点,如果是则直接返回
 if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null   key.equals(ek)))
 return e.val;
 }
 // 如果头结点  hash  值小于  0,说明是红黑树或者正在扩容,就用对应的  find  方法来查找
 else if (eh   0)
 return (p = e.find(h, key)) != null ? p.val : null;
 // 遍历链表来查找
 while ((e = e.next) != null) {
 if (e.hash == h  
 ((ek = e.key) == key || (ek != null   key.equals(ek))))
 return e.val;
 }
 }
 return null;
}

计算 Hash 值,并由此值找到对应的槽点;

如果数组是空的或者该位置为 null,那么直接返回 null 就可以了;

如果该位置处的节点刚好就是我们需要的,直接返回该节点的值;

如果该位置节点是红黑树或者正在扩容,就用 find 方法继续查找;

否则那就是链表,就进行遍历链表查找。

三:对比 Java7 和 Java8 的异同和优缺点 3.1 数据结构

Java 7 采用 Segment 分段锁来实现,而 Java 8 中的 ConcurrentHashMap 使用数组 + 链表 + 红黑树。

3.2 并发度

Java 7 中,每个 Segment 独立加锁,最大并发个数就是 Segment 的个数,默认是 16。

但是到了 Java 8 中,锁粒度更细,理想情况下 table 数组元素的个数(也就是数组长度)就是其支持并发的最大个数,并发度比之前有提高。

3.3 保证并发安全的原理

java 7 采用 Segment 分段锁来保证安全,而 Segment 是继承自 ReentrantLock。

Java 8 中放弃了 Segment 的设计,采用 Node + CAS + synchronized 保证线程安全。

3.4 遇到 Hash 碰撞

Java 7 在 Hash 冲突时,会使用拉链法,也就是链表的形式。

Java 8 先使用拉链法,在链表长度超过一定阈值时,将链表转换为红黑树,来提高查找效率。

3.5 查询时间复杂度

Java 7 遍历链表的时间复杂度是 O(n),n 为链表长度。

Java 8 如果变成遍历红黑树,那么时间复杂度降低为 O(log(n)),n 为树的节点个数。

上述内容就是 ConcurrentHashMap 在 Java7 和中的异同点是怎样的,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注丸趣 TV 行业资讯频道。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-25发表,共计4480字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)