ConcurrentHashMap的put源码解析
2019-05-16 18:31:23
阅读次数 - 823
label
Java
并发
大概走了一遍ConcurrentHashMap的put过程。源码是JDK 11的,跟JDK 8基本没有什么改动,但与JDK 7相差甚大。主要核心思想是以某个位置的头结点(链表的头结点或红黑树的root结点)为锁,配合自旋+CAS避免不必要的锁开销。
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key 和 value都不能为空
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); // 再哈希,防止用户自己写的哈希算法导致分布不均匀
int binCount = 0; // 这个位置元素的个数
// 死循环
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
// 如果没有初始化,就先进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 根据hash,取table上相应位置的第一个结点f,判断它是否为空
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果为空,用cas添加进去
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // 直接结束循环
}
// 如果当前节点是一个forwarding节点,表示在resize过程中
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 如果在resize过程中,帮助转换
else if (onlyIfAbsent // 检查第一个没有申请锁的结点
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
// 接下段代码块
}
}
// 元素计数加1,根据binCount来检验是否需要检查和扩容
// 如果<0,不会检查。这里不会小于0
// 如果<=1,会检查uncontended。
// 如果>1,会检查并尝试扩容
addCount(1L, binCount);
return null;
}
else {
V oldVal = null;
synchronized (f) { // 以第一个结点为锁
// 如果f是第一个结点
if (tabAt(tab, i) == f) {
// 如果f的hash大于0,说明不是在特殊状态,也不是红黑树
if (fh >= 0) {
binCount = 1;
// 死循环,每次binCount加1
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果当前节点的hash等于key的hashCode,
// 且当前节点的key equals key
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val; // 说明key已经存在,获取原来的值
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);
break; // 结束内层死循环
}
}
}
// 如果是红黑树结点
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
// 调用红黑树的put方法,其内部也是使用的CAS,并判断key是否已存在
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val; // key存在,得到原来值
if (!onlyIfAbsent)
p.val = value; // 设置新值
}
}
// 如果是保留的Node类型
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
// 如果binCount不为0,插入成功了,否则会进入再一次的循环
if (binCount != 0) {
// 如果大于转换红黑树的阈值
if (binCount >= TREEIFY_THRESHOLD)
// 转换成红黑树,内部以根结点为锁
treeifyBin(tab, i);
// 如果key已存在,有原值,返回原值
if (oldVal != null)
return oldVal;
break; // 结束外层死循环
}
}
Reference:
// 再hash, 跟HashMap的再哈希类似,让高位和低位同时参与
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
/*
* Encodings for Node hash fields. See above for explanation.
*/
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
static final class ReservationNode<K,V> extends Node<K,V> {
ReservationNode() {
super(RESERVED, null, null);
}
Node<K,V> find(int h, Object k) {
return null;
}
}
没有细看扩容相关的helpTransfer、addCount,以及初始化方法initTable,以后有机会再分析。
总结一下put的过程:
- 先检查key和value都不能为空,如果为空,就抛出空指针异常;
- 对key再哈希,防止用户自己写的哈希算法导致分布不均匀;
- 检查是否已经进行了初始化,如果没有,要先初始化;
- 根据再hash的值,计算在table上的位置的第一个结点f,如果f为空,用CAS直接插入;否则,进行第5步;
- 检查f是否是一个forwarding结点,如果是表示正在扩容,当前线程就去帮助扩容;如果不是,进行第6步;
- 检查f的key是不是等于要插入的key,如果是,获取原来的值并替换成新的值,如果不是,进行第7步;
- 以第一个结点为锁;
- 如果f不是红黑树结点,进行第9步,如果是红黑树结点,进行第11步;
- 根据链表向下找,如果找到key相等的位置,获取原来的值并替换成新的值;
- 如果一直到链表末尾也没有找到相等的key,就在链表尾部插入一个新节点;
- 调用红黑树的putTreeVal方法,其内部是使用CAS,也会判断key是否已经存在并返回旧值;
- 如果是保留的Node类型ReservationNode,直接抛出异常;
- 根据binCount判断是否达到链表转红黑树的阈值,如果达到了,转成红黑树,这个过程会以根节点为锁;
感谢阅读
觉得文章还不错?点击下方按钮分享吧!
评论
快来占个沙发吧~