ConcurrentHashMap --- putVal

首先贴 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值