ConcurrentHashMap --- 第一 cas 操作数组元素

这一生的挚爱 提交于 2020-03-27 15:51:31

首先贴 java 8 中实现的源代码

 

 1  /** Implementation for put and putIfAbsent */
 2     final V putVal(K key, V value, boolean onlyIfAbsent) {
 3         if (key == null || value == null) throw new NullPointerException();
 4         int hash = spread(key.hashCode());
 5         int binCount = 0;
 6         for (Node<K,V>[] tab = table;;) {
 7             Node<K,V> f; int n, i, fh;
 8             if (tab == null || (n = tab.length) == 0) // 第一次使用时,整个 table 为 null
 9                 tab = initTable();
10             else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
11                 if (casTabAt(tab, i, null,
12                              new Node<K,V>(hash, key, value, null)))
13                     break;                   // no lock when adding to empty bin
14             }
15             else if ((fh = f.hash) == MOVED)    // 当节点的 hash 值为 -1 时,代表这个节点为 ForwardingNode, 其后续节点需要转移
16                 tab = helpTransfer(tab, f);
17             else {
18                 V oldVal = null;
19                 synchronized (f) {
20                     if (tabAt(tab, i) == f) { // 再次获得热区代码执行权后,首先确保之前记录的第 i 个节点没有变化(即没被迁移走)
21                         if (fh >= 0) { // 确保当前节点的 hash 值代表正常的节点(其他的辅助型节点,比如 ForwardingNode,TreeBin,ReservationNode)
22                             binCount = 1;
23                             for (Node<K,V> e = f;; ++binCount) {
24                                 K ek;
25                                 if (e.hash == hash &&
26                                     ((ek = e.key) == key ||
27                                      (ek != null && key.equals(ek)))) { // 判断目标节点的根据是  spread 后的hash值相同并且(key 的 reference 相同或者 key 相等)
28                                     oldVal = e.val;
29                                     if (!onlyIfAbsent)
30                                         e.val = value;
31                                     break;
32                                 }
33                                 Node<K,V> pred = e;
34                                 if ((e = e.next) == null) { // 如果到达当前链表的尾节点,则 append 新的元素
35                                     pred.next = new Node<K,V>(hash, key,
36                                                               value, null);
37                                     break;
38                                 }
39                             }
40                         }
41                         else if (f instanceof TreeBin) { // 当 fh 小于 0,且为 -2 时,此时元素类型为 TreeBin
42                             Node<K,V> p;
43                             binCount = 2;
44                             if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
45                                                            value)) != null) {
46                                 oldVal = p.val;
47                                 if (!onlyIfAbsent)
48                                     p.val = value;
49                             }
50                         }
51                     }
52                 }
53                 if (binCount != 0) {
54                     if (binCount >= TREEIFY_THRESHOLD) // 当插入元素后,单个 slot 的链表的节点数目大于等于 8 时,进行红黑树化这个 slot
55                         treeifyBin(tab, i);
56                     if (oldVal != null)
57                         return oldVal;
58                     break;
59                 }
60             }
61         }
62         addCount(1L, binCount);
63         return null;
64     }

需要注意的几个关键点:

1. 如何从 key 定位其所在的 slot

我们常采用  (CAP-1) & spread(key.hashCode())  来确定我们要存储的 key 在 HashMap 底层存储的 index或者 slot (参见上述代码中的第 10 行)。获取某个key所对应的元素如下:

f = tabAt(tab, i=(n-1)&hash)

注意这里的 n 为当前 table 的大小。在 ConcurrentHashMap 的实现中, n 的默认大小为 16 (1<<<4) ,当发现当前 table 实际存储的大小大于等于n 等于 n - (n>>>2) ,即默认的 loadFactor 0.75 时,原来数组进行扩容,

变为原来的 2 倍,即 32, 之后依次累增,64,128,256,512,1024 ... 直到 1 <<< 30

2. table 的初始化

这块部分我们已经在另一篇随笔里面讲述了,参见  ConcurrentHashMap --- 怎样在高并发环境下初始化一个数组

3. slot 为空时添加元素

获取或者更新 table 中的元素时,我们采用 CAS 无锁的形式操作,具体方法为 tabAt,casTabAt,参见第 10, 11 行。至于为何要使用这种方式,参见 ConcurrentHashMap --- 第一 cas 操作数组元素

4. slot 不为空时添加元素

当 slot 不为空时,判断当前的 hash 值是否大于0 (ConcurrentHashMap为了在多线程环境下 resize引入了一些特殊的节点类型(ForwardingNode,TreeBin,ReservationNode),其hash值可能为负数)。

当 hash 值大于 0,即为正常的有意义的节点时,遍历以该节点为头的链表,如果找到对应的 key,则更新新值,否则append 新建的节点到链表的末尾。

遍历过程中判断 key 相等的方法如下:

e.hash && ((ek = e.key) == key || key.equals(ek))  

在这里额外插入一点。

假如,我们要存储的两个 key (k1, k2) 其实是相等的元素,但是我们只实现了 hashcode,而没有实现 equals 方法,则实际存储时,k1和 k2 会存放在一个 slot 的链表下面,

但是因为不相等,会存储为两个不同的元素

同理,假如两个 key (k1, k2) 其实是相等的元素,但是我们只实现了 equals,而没有实现 hashcode 方法,则实际存储时,k1和 k2 会存放在两个不同的 slot 下面

所为,如果认定两个 key 值可能引用不同,但值相等,我们既要重写其 equals 方法,又要重写其 hashCode 方法

5. 特殊元素的插入

遍历中如果发现元素为 TreeBin 类型,此时采用红黑树的插入实现

6. 树化

第 4 步插入元素后,可能链表过长,此时会将链表转换为树结构,见第 54 行

7. 增加 sizeCtl

sizeCtl 控制当前 table 可以添加的最大元素。每次添加元素后,统计当前 table 实际存储的元素个数,如果大于等于 sizeCtl,则进行扩容 resize

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!