上一篇分析了ConcurrentHashMap中的属性和构造器,这一篇记录下ConcurrentHashMap的put方法
方法
put 添加元素
/**
* 添加一个元素
*/
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) {
if (key == null || value == null) throw new NullPointerException();
// 计算hash值
int hash = spread(key.hashCode());
int binCount = 0;
// 这里是一个死循环
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
// 这里才是数组真正的初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果要插入的元素所在的桶还没有元素,就把这个元素插入到这个桶中
// 桶就是这个数组下标位置的链表或树
// 注意高效取模运算 hash&(n-1) == hash%n (n是2的m次幂)
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
// cas插入,如果成功插入了就break跳出循环,如果失败了,下次循环处理
// 成功代表cas插入时桶里仍然没有数据,也就是桶中第一元素是null
// 失败表示cas插入时桶里有数据了
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
// 如果要插入元素的桶的第一个元素的hash是MOVED,说明可能有扩容,当前线程帮忙一起迁移元素
tab = helpTransfer(tab, f);
else {
// 如果当前桶中有元素,且不是在扩容,就先锁住这个桶(分段锁)
// 然后查找当前要插入的元素在这个桶中是否已经存在了
// 如果存在,就替换掉
// 不存在,就插入到链表尾部,或树中
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
// 如果第一个元素的hash值大于等于0,说明没在扩容,也不是树,那么桶就是链表了
// 桶中的元素个数,记为1
binCount = 1;
// 死循环遍历桶,这里通过++binCount计算链表中的元素个数,这个值关系到链表转红黑树
// 这里可能有一个疑惑,如果桶没有遍历完,就找到了元素,那么++binCount得到的并不是桶中元素的总个数,是这样没错
// 但其实如果出现桶还没遍历完就找到了元素的情况,及当前元素还有next节点,说明在本次map的put之前,已经有和当前元素不同的元素放了进来,那么在那些元素put进来的时候同样进行了这个循环
// 如果那时++binCount已经达到了转树的条件,则本次就不会进这里(链表逻辑)了
// 如果那时++binCount还未达到转树条件,而本次循环在桶中找到了相同key的元素,则本次也不会满足转树的条件
// 那么逻辑就简单了,只有这里的循环到最后仍找不到元素,当前元素被插入到链表尾部,才有可能达到转树的条件
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 从桶中查找这个元素,如果找到了
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 缓存旧值
oldVal = e.val;
if (!onlyIfAbsent)
// 新值替换旧值
e.val = value;
// 循环结束
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
// 如果到了链表尾部还没有找到
// 就把这个元素插入的链表尾部
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 如果桶是一棵树
else if (f instanceof TreeBin) {
Node<K,V> p;
// 记桶中元素个数为2
binCount = 2;
// 调用红黑树的插入方法
// 如果插入成功返回null,插入失败返回找到的节点
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
// 临时缓存旧的值
oldVal = p.val;
if (!onlyIfAbsent)
// 新值替换旧值
p.val = value;
}
}
}
}
// 如果binCount不是0,说明成功插入元素或找到了元素
if (binCount != 0) {
// 如果链表中的元素个数达到了8,将链表转为红黑树
// 如果链表已经树化了,但由于上面的树逻辑只为binCount赋值了2,没有计算树中的元素个数,达不到转树条件,所以不会重复树化
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
// 如果要插入的元素已经存在,就返回旧值
return oldVal;
// 跳出最外层循环
break;
}
}
}
// 成功插入了元素,元素个数加一
addCount(1L, binCount);
// 成功插入了元素返回null
return null;
}
/**
* 初始化table
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
// 如果sc < 0就yield,当前线程不进行初始化
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 这里compareAndSwap使用sc而不是sizeCtl的原因是sizeCtl被volatile修饰,
// 如果有线程修改了sizeCtl的值,其他线程是可见的,所以先把sizeCtl赋值给局部变量sc,以此防止并发
try {
if ((tab = table) == null || tab.length == 0) {
// 数组的长度,如果没传,就是16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// sc = n-n/4 = n*0.75
// 所以负载因子写死了0.75,传进来也没用
sc = n - (n >>> 2);
}
} finally {
// 此时sizeCtl就是扩容阈值了
sizeCtl = sc;
}
break;
}
}
return tab;
}