简单了解 ConcurrentHashMap 在 JDK7 和 JDK8 中的区别

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情

在了解 HashMap 的的原理时,对于 jdk7 和 8 的实现是不同的,同样,对于支持并发的 ConcurrentHashMap 来说其实现也不相同。

其主要区别在于两者保证线程安全的机制不同,jdk7 采用的是分段锁的概念,每一个分段都有一把锁,锁内存储的着数据,锁的个数在初始化之后不能扩容。

而 jdk8 的 ConcurrentHashMap 数据结构同 HashMap,通过 Synchronized+CAS 来保证其线程安全。

jdk7

在 jdk7 中,有一个非常重要的概念就是 Segment,实际上我们发现,同 HashMap 的设计一样,它也是用来存储数据的一个变量。

/**
 * The segments, each of which is a specialized hash table.
 * 表示 每一段都是一个hash表
 */
final Segment<K,V>[] segments;
复制代码

Segment 这个内部类中,有一个 table 变量,在 HashMap 中存储数据也是叫 table 的变量。

image-20220120104609604

这里叫 HashEntry,而 HashMap 中就叫 Entry,其内部成员变量都大致相同,HashEntry 如下:

static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
    HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    ......   
}    
复制代码

再去看Segment这个类:

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    ......
}
复制代码

可以看到它继承了 ReentrantLock,因此可以实现加锁操作,而Segment有段、片的意思,因此通常叫做分段锁。

所以我们可以得出 ConcurrentHashMap 的结构大致为:

ConcurrentHashMap 是由一个个 Segment 组成的,并且每一个 Segment 包含了一个 HashEntry 数组,数组中的每一个 HashEntry 就是存储的数据。

用一张图来描述它:

image-20220120111633580

结构知道了,我们现在看看 put()方法:

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    // 1.为输入的key做 hash 运算,得到 hash 值;
    int hash = hash(key);
    // 2.通过hash值,定位到对应的 Segment 对象;
    int j = (hash >>> segmentShift) & segmentMask;
    // 3.检查segment[j]是否已经初始化了,没有的话调用ensureSegment初始化segment[j]
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    // 4.向片段中插入键值对,加锁操作
    return s.put(key, hash, value, false);
}
复制代码
  1. 为输入的 key 做 hash 运算,得到 hash 值;
  2. 通过 hash 值,定位到对应的 Segment 对象;
  3. 检查 segment[j] 是否已经初始化了,没有的话调用 ensureSegment 初始化 segment[j];
  4. 向片段中插入键值对(加锁操作)。

这里不想深入了,太复杂,想了解的话可参考:ConcurrentHashMap 1.7 源码解读

jdk8

在看看 jdk8 版本,他主要做了 2 处改动:

  1. 取消 segments 字段,直接采用transient volatile Node<K,V>[] table保存数据,采用 table 数组元素作为锁,从而实现了对每一行数据进行加锁,并发控制使用 Synchronized 和 CAS 来操作;
  2. 将原先数组+单向链表的数据结构,变更为数组+单向链表+红黑树的结构。

不同于 jdk7 的HashEntry,jdk8 中叫 Node,结构类似:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    ......
}
复制代码

还有用于存储红黑树的数据的存储结构 TreeNode:

static final class TreeNode<K,V> extends Node<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next,
             TreeNode<K,V> parent) {
        super(hash, key, val, next);
        this.parent = parent;
    }
    ......
}
复制代码

用一张图来描述它的结构:

image-20220120171554803

这个结构和 HashMap 的结构实现基本一致,只是为了保证线程安全而使得其实现变复杂。

put()方法

image-20220120192959410

① 根据 key 计算出 hashcode ;

② 判断是否需要进行初始化;

f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功;

④ 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容,相对于 HashMap 要复杂很多;

⑤ 如果都不满足,则利用 synchronized 锁写入数据;

⑥ 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

在 ConcurrentHashMap 中通过一个Node<K,V>[]数组来保存添加到 map 中的键值对,而在同一个数组位置是通过链表和红黑树的形式来保存的。但是这个数组只有在第一次添加元素的时候才会初始化,否则只是初始化一个ConcurrentHashMap 对象的话,只是设定了一个sizeCtl变量,这个变量用来判断对象的一些状态和是否需要扩容。

第一次添加元素的时候,默认初期长度为 16,当往 map 中继续添加元素的时候,通过 hash 值跟数组长度取与来决定放在数组的哪个位置,如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了 8 个以上,如果数组的长度还小于 64 的时候,则会扩容数组。如果数组的长度大于等于 64 了的话,在会将该节点的链表转换成树。

通过扩容数组的方式来把这些节点给分散开。然后将这些元素复制到扩容后的新的数组中,同一个链表中的元素通过 hash 值的数组长度位来区分,是还是放在原来的位置还是放到扩容的长度的相同位置去 。在扩容完成之后,如果某个节点的是树,同时现在该节点的个数又小于等于6个了,则会将该树转为链表。

get()方法

取元素的时候,相对来说比较简单,通过计算 hash 来确定该元素在数组的哪个位置,然后在通过遍历链表或树来判断 key 和 key 的 hash,取出 value 值。

这篇文章并没有去对源码进行一行行的分析(因为太复杂了,目前对我来说稍微有点难度,并且暂时不想花太多时间在上面),只是参考一些大佬的文章并了解了一下 2 个版本的差异,而对于为什么要重写,个人觉得还是效率等问题,虽然代码量从 jdk7 的 1000 多行变为了 jdk8 的 6000 多行,并且 jdk8 中使用 Synchronized 而不是 ReentrantLock。jdk8 之前都说 synchronized 属于重量级锁,但 jdk8 做了优化之后性能并不会比 ReentrantLock 差,况且根据其结构对比,锁的粒度要减小,是单独对一个 Node 上锁。

参考

blog.csdn.net/weixin_4318…

blog.csdn.net/woaiwym/art…

猜你喜欢

转载自juejin.im/post/7107047514771685389