Java - 深入探究ConcurrentHashMap(二)

基于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为数组长度。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值