嘿嘿,我就知道面试官接下来要问我 ConcurrentHashMap 底层原理了,看我怎么秀他 (6)

这个方法逻辑比较复杂,会一直循环尝试获取锁,若获取成功,则返回。否则的话,每次循环时,都会同时遍历当前链表。若遍历完了一次,还没找到和key相等的节点,就会预先创建一个节点。注意,这里只是预测性的创建一个新节点,也有可能在这之前,就已经获取锁成功了。

同时,当重试次每偶数次时,就会检查一次当前最新的头结点是否被改变。因为若有变化的话,还需要从最新的头结点开始遍历链表。

还有一种情况,就是循环次数达到了最大限制,则停止循环,用阻塞的方式去获取锁。这时,也就停止了遍历链表的动作,当前线程也不会再做其他预热(warm up)的事情。

关于为什么预测性的创建新节点,源码中原话是这样的:

Since traversal speed doesn't matter, we might as well help warm up the associated code and accesses as well.

解释一下就是,因为遍历速度无所谓,所以,我们可以预先(warm up)做一些相关联代码的准备工作。这里相关联代码,指的就是循环中,在获取锁成功或者调用 lock 方法之前做的这些事情,当然也包括创建新节点。

在put 方法中可以看到,有一句是判断 node 是否为空,若创建了,就直接头插。否则的话,它也会自己创建这个新节点。

嘿嘿,我就知道面试官接下来要问我 ConcurrentHashMap 底层原理了,看我怎么秀他

scanAndLockForPut 这个方法可以确保返回时,当前线程一定是获取到锁的状态。

rehash()方法

当 put 方法时,发现元素个数超过了阈值,则会扩容。需要注意的是,每个Segment只管它自己的扩容,互相之间并不影响。换句话说,可以出现这个 Segment的长度为2,另一个Segment的长度为4的情况(只要是2的n次幂)。

//node为创建的新节点 private void rehash(HashEntry<K,V> node) { //当前Segment中的旧表 HashEntry<K,V>[] oldTable = table; //旧的容量 int oldCapacity = oldTable.length; //新容量为旧容量的2倍 int newCapacity = oldCapacity << 1; //更新新的阈值 threshold = (int)(newCapacity * loadFactor); //用新的容量创建一个新的 HashEntry 数组 HashEntry<K,V>[] newTable = (HashEntry<K,V>[]) new HashEntry[newCapacity]; //当前的掩码,用于计算节点在新数组中的下标 int sizeMask = newCapacity - 1; //遍历旧表 for (int i = 0; i < oldCapacity ; i++) { HashEntry<K,V> e = oldTable[i]; //如果e不为空,说明当前链表不为空 if (e != null) { HashEntry<K,V> next = e.next; //计算hash值再新数组中的下标位置 int idx = e.hash & sizeMask; //如果e不为空,且它的下一个节点为空,则说明这条链表只有一个节点, //直接把这个节点放到新数组的对应下标位置即可 if (next == null) // Single node on list newTable[idx] = e; //否则,处理当前链表的节点迁移操作 else { // Reuse consecutive sequence at same slot //记录上一次遍历到的节点 HashEntry<K,V> lastRun = e; //对应上一次遍历到的节点在新数组中的新下标 int lastIdx = idx; for (HashEntry<K,V> last = next; last != null; last = last.next) { //计算当前遍历到的节点的新下标 int k = last.hash & sizeMask; //若 k 不等于 lastIdx,则说明此次遍历到的节点和上次遍历到的节点不在同一个下标位置 //需要把 lastRun 和 lastIdx 更新为当前遍历到的节点和下标值。 //若相同,则不处理,继续下一次 for 循环。 if (k != lastIdx) { lastIdx = k; lastRun = last; } } //把和 lastRun 节点的下标位置相同的链表最末尾的几个连续的节点放到新数组的对应下标位置 newTable[lastIdx] = lastRun; //再把剩余的节点,复制到新数组 //从旧数组的头结点开始遍历,直到 lastRun 节点,因为 lastRun节点后边的节点都已经迁移完成了。 for (HashEntry<K,V> p = e; p != lastRun; p = p.next) { V v = p.value; int h = p.hash; int k = h & sizeMask; HashEntry<K,V> n = newTable[k]; //用的是复制节点信息的方式,并不是把原来的节点直接迁移,区别于lastRun处理方式 newTable[k] = new HashEntry<K,V>(h, p.key, v, n); } } } } //所有节点都迁移完成之后,再处理传进来的新的node节点,把它头插到对应的下标位置 int nodeIndex = node.hash & sizeMask; // add the new node //头插node节点 node.setNext(newTable[nodeIndex]); newTable[nodeIndex] = node; //更新当前Segment的table信息 table = newTable; }

上边的迁移过程和 lastRun 和 lastIdx 变量可能不太好理解,我画个图就明白了。以其中一条链表处理方式为例。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsxgxf.html