基于jdk1.8
ConcurrentHash扩容-数据迁移阶段
- 扩容阶段源码注释版本
// 对目标节点位置加锁,开始处理数据
synchronized (f) {
// 双重校验
if (tabAt(tab, i) == f) {
// ln:低位节点 hn:高位节点
Node<K,V> ln, hn;
if (fh >= 0) {
// fh:当前节点hash值 n:原数组长度
// 计算runBit的有两种情况 1.等于零 2.不等于零
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);
// 高位链路移动到原位置加n位置 例如对位置为10的链表进行迁移,高位链迁移后的位置为26
setTabAt(nextTab, i + n, hn);
// 将处理过的位置放置fwd节点,表示该位置已经被处理过了
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
// ....红黑树逻辑实现
}
}
}
ConcurrentHashMap—精华提炼
- 切入点
- 通过 https://blog.csdn.net/GoNewWay/article/details/105346064 的分析,可以了解到学习ConcurrentHashMap的切入点在put方法
- 精华提炼
Question—HashMap为什么要进行hash再运算?
Answer
ConcurrentHashMap中调用put方法,通过spread方法对元素的hashCode值做再一次运算。元素的hash值通过该运算计算过后,最高位一定为0。把计算的结果控制在int最大整数之内。
// h >>> 16 将元素的hash值无符号右移16位,即高位全是零
// h ^ (h >>> 16) 做异或运算
// & HASH_BITS 即 & 0x7fffffff 做“位与”运算
// 0x7fffffff 表示int类型的最大整型数 即在2进制中除了首位都是1 进行 & 运算后,最高位必为0
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
为了便于理解,做一个伪运算。令 h 为 1111 1101 1111 1010 0010 1100 1011 0010
1111 1101 1111 1010 0010 1100 1011 0010
0000 0000 0000 0000 1111 1101 1111 1010 // h >>> 16
1111 1101 1111 1010 1101 0001 0100 1000 // h ^ (h >>> 16)
0111 1111 1111 1111 1111 1111 1111 1111 // 0x7fffffff
// 最终结果
0111 1101 1111 1010 1101 0001 0100 1000 // (h ^ (h >>> 16)) & HASH_BITS
这样做的目的主要是混合了元素的 高16位和低16位,最终计算得到的结果具备了元素原高位和低位的特征。使hash值更加不确定来降低碰撞的概率。该算法又被成为 扰动函数/扰动算法。
Question—ConcurrentHashMap什么时候进行了数组的初始化?
Answer
ConcurrentHashMap的构造方法并没有对数组进行初始化。以传入初始化大小参数的ConcurrentHashMap为例。这里设置了sizeCtl的值。代表下一次扩容的大小。初始化数组的操作延伸到第一次put操作。
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
Question–sizeCtl属性的意义?
Answer
- 负数 代表正在进行初始化或扩容操作
- -1 代表正在初始化
- -N 表示有N-1个线程正在进行扩容操作
- 正数或0 代表hash表还没有被初始化,表示初始化或下一次进行扩容的大小。始终是ConcurrentHashMap容量的0.75倍,与扩容因子 loadfactor 对应。
Question–ConcurrentHashMap如何取得数组下标,并实现并发场景下安全插入元素?
Answer
元素的hash值通过扰动函数进行再一次运算后,再和 n-1 做 与 运算。取得数组下标。
// n-1 :数组长度减一
i = (n - 1) & hash
实现并发场景下插入元素。
通过cas操作把元素封装成NODE,插入到tab[i]位置。
- 当有一个线程cas操作成功之后退出循环,由于cas是原子操作。其它线程也能看到**tab[i]**位置被成功插入了元素。从而实现了线程安全。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
-
若是**tab[i]**位置已经有了元素,则会形成链表。此时会有目标节点进行加锁,并不影响其它节点进行数据的插入。
-
若当前节点是TreeBin类型节点,说明当前节点是红黑树根节点,则会在数结构上遍历元素,更新或插入。
Question–发生hash碰撞时的处理方式,同一链表上的元素hash值相同吗?
Answer
发生hash碰撞时,会在目标数组上构造链表。实现数组+链表的数据结构。如果链表的长度大于8,则会去执行扩容或链转红黑树操作。当链表的长度大于8,数组长度大于64,链转红黑树。若数组长度小于64,则执行扩容操作。
ConcurrentHashMap得到数组下标位置的方式是 经过扰动函数运算得到的 hash 值 和 数组长度减一 做 与 运算。所以,同一链表上的元素的hash值不一定相同。
Question–ConcurrentHashMap如何统计元素个数的?
Answer
ConcurrentHashMap统计元素个数,分为并发场景下计数和非并发场景下的计数。非并发场景下,使用全局变量记录插入数组的元素个数。并发场景下,使用CounterCell数组来统计元素个数,这里采用了分而治之的思想,当多个线程同时进行插入数据操作的时候。CounterCell数组中的每一个元素都会被用来存储元素个数。具体做法是:通过**ThreadLocalRandom.getProbe()**方法会为每一个线程分配一个唯一的随机数值,通过计算,每一个线程会去操作数组中的不同位置,分别记录自己插入成功的元素个数,记录到指定数组位置。
Question–什么情况下会触发链转红黑树?
Answer
当某一节点位置的链表长度大于8的时候,会触发链转红黑树的操作。但是并不会立即转化为红黑树,而是会进一步判断数组的长度是否大于64。如果数组长度大于64,则进行链转红黑树的操作。否则进行扩容,把链表拆分
成高低链,低位链位置保持不变,高位链位置向后移动 n。n为数组长度。