小知识,大挑战!本文正在参与「程序员必备小知识」创作活动
01
transfer方法
这个方法是真正的扩容方法,扩容时容量变为原来的两倍,并把部分元素迁移到其他桶中,整体流程图
private final void transfer(Node<K, V>[] tab, Node<K, V>[] nextTab) {
// n:当前数组长度
// stride表示分配给线程的任务步长,通俗点说就是分配数组的一段位置让他去帮忙转移数据到新数组
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//表示当前线程为触发本次扩容的线程,需要做些扩容前的准备
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
//初始化新的数组赋值给nextTab
Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
// 记录迁移数据整体进度的标记,从1开始计算
transferIndex = n;
}
//新数组的长度
int nextn = nextTab.length;
//fwd结点,当某个桶位数据处理完毕,将其设置为fwd节点,写线程和读线程看到后会有不同的处理逻辑
ForwardingNode<K, V> fwd = new ForwardingNode<K, V>(nextTab);
//推进标记
boolean advance = true;
//完成标记
boolean finishing = false; // to ensure sweep before committing nextTab
//i :代表当前线程处理的桶位
// bound :表示当前线程所能处理的桶位下界
for (int i = 0, bound = 0; ; ) {
Node<K, V> f;
int fh;
/**
* 1. 给当前线程分配任务区间
* 2. 维护当前线程任务进度
* 3. 维护map对象全局范围内的进度
*/
while (advance) {
// 分配任务区间的变量
int nextIndex, nextBound;
//--i >= bound 表示当前线程是否完成,如果>=bound,就代表当前线程还有相应区间的桶位没有完成
if (--i >= bound || finishing)
advance = false;
// 当前线程已完成或者未分配
// 条件成立表示全局范围的桶位都已经分配完毕
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 当前线程需要分配任务区间
// 全局范围内还有桶位尚未迁移
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//i<0表示当前线程未分配到任务
if (i < 0 || i >= n || i + n >= nextn) {
//保存sizeCtl的变量
int sc;
//
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 条件成立,说明设置sizeCtl 低16位-1 成功,当前线程可以正常退出
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
//前置条件:【CASE2~CASE4】 当前线程任务尚未处理完,正在进行中
//CASE2:
//条件成立:说明当前桶位未存放数据,只需要将此处设置为fwd节点即可。
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//CASE3:
//条件成立:说明当前桶位已经迁移过了,当前线程不用再处理了,
// 直接再次更新当前线程任务索引,再次处理下一个桶位 或者 其它操作
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
//CASE4:
//前置条件:当前桶位有数据,而且node节点 不是 fwd节点,说明这些数据需要迁移。
else {
//sync 加锁当前桶位的头结点
synchronized (f) {
//防止在你加锁头对象之前,当前桶位的头对象被其它写线程修改过,导致你目前加锁对象错误..
if (tabAt(tab, i) == f) {
//ln 表示低位链表引用
//hn 表示高位链表引用
Node<K, V> ln, hn;
//表示当前桶位是链表桶位
if (fh >= 0) {
int runBit = fh & n;
Node<K, V> lastRun = f;
for (Node<K, V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
//等于代表当前节点属于低位链表
if (runBit == 0) {
ln = lastRun;
hn = null;
} else {
hn = lastRun;
ln = null;
}
for (Node<K, V> p = f; p != lastRun; p = p.next) {
int ph = p.hash;
K pk = p.key;
V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K, V>(ph, pk, pv, ln);
else
hn = new Node<K, V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
// 表示当前桶位是红黑树代理结点TreeBin
else if (f instanceof TreeBin) {
TreeBin<K, V> t = (TreeBin<K, V>) f;
TreeNode<K, V> lo = null, loTail = null;
TreeNode<K, V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K, V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K, V> p = new TreeNode<K, V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
} else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K, V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K, V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
1 创建新桶数组,新桶数组大小是旧桶数组的两倍
2 为当前线程分配迁移的桶位,迁移元素先从靠后的桶开始
3 迁移完成的桶将头结点设置为ForwardingNode类型的元素,标记该桶迁移完成
4 迁移时会进行加锁,然后根据hash&n判断是否等于0把桶中元素分化成两个链表或树
5 低位链表存储在原来的下标位置
6 高位链表 存储在原来位置加n的位置
02
删除元素 remove方法
public V remove(Object key) {
return replaceNode(key, null, null);
}
remove方法其实就是调用replaceNode方法,这个方法除了会进行删除操作,也提供替换元素的功能,区分删除和替换就看传的value值是否为空。
final V replaceNode(Object key, V value, Object cv) {
// 计算hash
int hash = spread(key.hashCode());
//自旋
for (Node<K, V>[] tab = table; ; ) {
// f 当前Node结点
// n 数组长度
// i 当前key数组下标
// fh key经过扰动的哈希值
Node<K, V> f;
int n, i, fh;
// 数组还没初始化或者目标Key的桶不存在就跳出
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
// 如果正在扩容,就协助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 标记是否处理过
boolean validated = false;
// 锁住当前桶位头结点
synchronized (f) {
//再次验证当前桶第一个元素是否被修改过
if (tabAt(tab, i) == f) {
// hash值大于等于0表示是链表结点
if (fh >= 0) {
validated = true;
// for循环该链表结点寻找目标结点
for (Node<K, V> e = f, pred = null; ; ) {
K ek;
//遍历链表寻找目标节点
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 找到目标节点值
V ev = e.val;
// 检查目标节点旧value是否和cv相等
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
//不为空说明是替换操作,不是删除操作
if (value != null)
// 替换旧值
e.val = value;
//
else if (pred != null)
// 如果前置节点不为空
// 删除当前节点
pred.next = e.next;
else
// 如果前置节点为空
// 说明是桶中第一个元素,进行删除
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
// 遍历链表尾部还没找到元素,跳出循环
if ((e = e.next) == null)
break;
}
}
// 如果是树结点,就按照红黑树的方式寻找目标节点
// 并进行替换或删除操作
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K, V> t = (TreeBin<K, V>) f;
TreeNode<K, V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
// 如果处理过,不管有没有找到元素都返回
//什么时候会是false呢,当其他线程修改头节点时
// 该线程锁住的头节点时错的,这时validated就是false,会进入下一次for循环自旋操作
if (validated) {
// 如果找到元素,返回其旧值
if (oldVal != null) {
// 如果要替换的值为空,元素个数减1
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
-
首先计算hash值,然后进行一个自旋的操作,只所以会有自旋的操作,是因为可能当期线程要操作的桶位元素被别的线程占用,那就要通过自旋再下次循环尝试操作。
-
如果当前map正在扩容,就先协助扩容,再进行删除操作。
-
获取当前桶位的锁,判断是链表还是红黑树
-
桶位头节点hash值大于等于0就是链表结点,遍历寻找目标节点,进行替换或删除操作
-
如果是树结点就按照红黑树的方式寻找目标节点进行替换或删除操作
-
最后如果删除成功,要调用addCount对map元素数量减一。
02
获取元素 get方法
获取元素有个地方要重点说一下就是它会根据key所在桶的头结点的不同采取不同的方式获取元素,关键的地方就是find()方法的调用
public V get(Object key) {
// 引用 map.table
Node<K, V>[] tab;
// e;当前元素节点
// p 目标元素节点
Node<K, V> e, p;
// n 数组长度
// eh 当前元素hash值
int n, eh;
// 当前元素key
K ek;
// 计算hash值
int h = spread(key.hashCode());
// 如果元素所在的桶存在且里面有元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果第一个元素就是要找的元素,直接返回
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 如果当前节点hash值小于0,说明是树或者正在扩容
else if (eh < 0)
// 使用find寻找元素,find寻找方式依据Node的不同子类有不同的实现方式
// 可能是FWD类型,就会调用FWD的find方法
// 可能是TreeBin类型,就会调用TreeBin的find方法
return (p = e.find(h, key)) != null ? p.val : null;
// 遍历链表寻找元素
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
-
如果桶位第一个元素就是要的元素,直接返回
-
如果当前节点hash值小于0,说明是树或者正在扩容
-
如果正在扩容就调用ForwardingNode的find方法寻找目标节点
-
如果是树,就调用TreeBind 的 find方法获取目标节点
-
hash值不小于0说明是链表,就遍历寻找目标节点
03
ForwardingNode的find方法
Node<K, V> find(int h, Object k) { // loop to avoid arbitrarily deep recursion on forwarding nodes outer: for (Node<K, V>[] tab = nextTable; ; ) { // e :表示在扩容而创建新表使用询址算法得到的桶位头节点 // n:表示为扩容而创建的新表的长度 Node<K, V> e; int n; // 如果tab等于空,或者当前桶位等于空,就返回空 if (k == null || tab == null || (n = tab.length) == 0 || (e = tabAt(tab, (n - 1) & h)) == null) return null; //自旋 for (; ; ) { // eh:新扩容后表指定桶位的当前节点的hash // ek 新扩容后表指定桶位的当前节点的key int eh; K ek; //在新扩容的表中寻找到了元素 if ((eh = e.hash) == h && ((ek = e.key) == k || (ek != null && k.equals(ek)))) return e; // 小于0,可能是TreeBin类型 // 可能是FWD类型(新扩容的表,在并发很大的情况下,可能再次扩容,然后再次拿到FWD类型) if (eh < 0) { if (e instanceof ForwardingNode) { tab = ((ForwardingNode<K, V>) e).nextTable; continue outer; } else // 说明桶位是TreeBin节点 return e.find(h, k); } //前置条件:当前桶位头结点 并没有命中查询,说明此桶位是 链表 //1.将当前元素 指向链表的下一个元素 //2.判断当前元素的下一个位置 是否为空 // true->说明迭代到链表末尾,未找到对应的数据,返回Null if ((e = e.next) == null) return null; } }}
-
将tab指向新扩容的数组
-
查询当前节是否是目标节点,是的话就返回
-
判断当前节点hash值小于0,可以看到这里又进行了一次这样的判断,这是因为在并发很大的情况下,可能扩容完不久又进行了扩容操作
判断链表next节点是否为空,是的话说明遍历到了链表尾部都诶有找到元素,返回空。
总结
(1)ConcurrentHashMap是HashMap的线程安全版本;
(2)ConcurrentHashMap采用(数组 + 链表 + 红黑树)的结构存储元素;
(3)ConcurrentHashMap相比于同样线程安全的HashTable,效率要高很多;
(4)ConcurrentHashMap采用的锁有 synchronized,CAS,自旋锁,分段锁,volatile等;
(5)ConcurrentHashMap中没有threshold和loadFactor这两个字段,而是采用sizeCtl来控制;
(6)sizeCtl = -1,表示正在进行初始化;
(7)sizeCtl = 0,默认值,表示后续在真正初始化的时候使用默认容量;
(8)sizeCtl > 0,在初始化之前存储的是传入的容量,在初始化或扩容后存储的是下一次的扩容门槛;
(9)sizeCtl = (resizeStamp << 16) + (1 + nThreads),表示正在进行扩容,高位存储扩容邮戳,低位存储扩容线程数加1;
(10)更新操作时如果正在进行扩容,当前线程协助扩容;
(11)更新操作会采用synchronized锁住当前桶的第一个元素,这是分段锁的思想;
(12)整个扩容过程都是通过CAS控制sizeCtl这个字段来进行的,这很关键;
(13)迁移完元素的桶会放置一个ForwardingNode节点,以标识该桶迁移完毕;
(14)元素个数的存储也是采用的分段思想,类似于LongAdder的实现;
(15)元素个数的更新会把不同的线程hash到不同的段上,减少资源争用;
(16)元素个数的更新如果还是出现多个线程同时更新一个段,则会扩容段(CounterCell);
(17)获取元素个数是把所有的段(包括baseCount和CounterCell)相加起来得到的;
(18)查询操作是不会加锁的,所以ConcurrentHashMap不是强一致性的;
(19)ConcurrentHashMap中不能存储key或value为null的元素;