JUC1.8-ConcurrentHashMap源码学习-putVal()方法

前言

在进入主题前,先用一段描述性的话,来帮助理解put的整体思路:

需求: A盒子[容量不知],N物品。 现在让N物品放入到A盒子中;

过程:
1. 首先要先有一个N物品;
2. 在给N物品打上在A盒子的位置坐标,方便我后面获取【类似去超市,储物柜道理】;
3. 在看看有没有A盒子。如没有,就去得造一个A盒子,放物品的人,没告诉需要多大容量的盒子,那么默认造一个2次幂的容量的盒子。 注意:这时可能不是我一个人的N物品向往盒子里放入,如果有人在我之前已经在造盒子,那我别浪费功夫,我就等他造好直接用就行了。
4. A盒子造好后,就看看第二步中,N物品在A盒子的坐标位置上有没有物品已经存放了? 没有,那么直接放入,那这个需求也就完成了;
5. 但可能N物品在A盒子的坐标位置上已经有N+1物品存放,那么就锁定N+1物品这列,先一个一个去对比,N+1的物品与目前要放的N物品是不是一致的,是,则替换。 反则按照不同的规则【源码细讲】将其放在其后方即可;
6. 放完N物品后,要检查下这个A盒子的容量是不是快满了,如果达到这个盒子的百分之75,那索性就给你扩大一些。
7. 那还要在想,如果A盒子的东西很多很多,我一个人扩大完体积后,还得在搬运会不会太慢了? 答案是:肯定得,那我就加个标志,告诉后面在想放东西进来的人,我现在在为人民服务,把A盒子容量扩大,你们看见后,来一起搭把手,速度把A盒子扩好后,在用第四,五步的方法放入物品;

带着问题看源码:

经过上面蹩脚的描述,大家脑海有这个画面了吧。 那么现在就要用计算机的语言造出来,带着这么几个问题去看:

咳咳,这个就是本人在阅读过程中,脑海里缠绕的问题。问题比较多,部分问题,会在后期讲扩容时,回答。

ConcurrentHashMap主要数据结构:

/** 获取当前机器cpu的数量 */
NCPU = Runtime.getRuntime().availableProcessors();

/** 扩容时,储存临时tables,容量:原容量的2倍 */
volatile Node<K,V>[] nextTable;

/** 容量大小,通过CAS更新 */
volatile long baseCount;

/** 
   *控制线程扩容和初始化的重重重要参数,有一些参数值:
   *0: 默认值;
   *-1:table 正在初始化;
   *-N:N-1个线程在协助扩容;
   *基于初始化情况:
   * 完成时:table的容量,默认是容量*0.75 。实际table的公式:n - (n >>> 2)
   *未完成时:table需要初始化的大小;
   */
volatile int sizeCtl;

/*
 * 用来返回节点数组的指定位置的节点的原子操作
 */
@SuppressWarnings("unchecked")
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

/*
 * cas原子操作,在指定位置设定值
 */
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
/*
 * 原子操作,在指定位置设定值
 */
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

putVal()方法解析阅读:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    //非空判断
    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;//存放hashcode一致的node
        int n,      //当前table的长度
            i,      //table的index值
            fh;     //f节点的hash值

        if (tab == null || (n = tab.length) == 0) //没有tables
            //初始化2次幂的table
            tab = initTable();

        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //给Node<K,V> f赋值 ---注意这点使用位运算(n-1)&hash 相对于hash%n取余. 并且与为什么容量是2次幂有莫大关系,后面分析.
            //如果没有数据,则使用cas无加锁方式,插入
            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin

        }else if ((fh = f.hash) == MOVED) //hash为-1 扩容标志,说明是一个ForwardingNode,后面跟上的新的tab
            //扩容后,并赋予tab;
            tab = helpTransfer(tab, f);

        else {//通过(n-1)&hash取余后,发现该index位置有值,俗称hash碰撞了. 那么来看看是怎么运用链表和数处理

            V oldVal = null;
            synchronized (f) {//锁住hash碰撞的这一列f节点[与之前区别,之前是锁一段tab,现在只是锁某一列,锁的颗粒变小了],防止增加链表的时候导致链表成环
                if (tabAt(tab, i) == f) {//在次检查对应的index位置节点没有改变
                    if (fh >= 0) {// f节点的hash值不小于0时

                        //链表的初始化长度为1
                        binCount = 1;
                        //++binCount 死循环到链表尾部,将值插入
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //判断hash值与key是否相等,是则替换并跳出;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }

                            //前一步判断不相等,那么判断当前node的next节点为空,则放入,否者将nextnode替换成当前循环node循环比对
                            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) {//f节点的hash小于0 并且是数结构
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }

            if (binCount != 0) { //这块会做俩件事,通过binCount判断是否到转数阀值8,且总体容量超过64,才会转成红黑树.   如果达到转数阀值8,总容量没有到64,咋会进行扩容
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }

    //计算估计容量数,超过容量阀值,进行扩容
    addCount(1L, binCount);
    return null;
}

套用最开始描述话,在来理解下,具体过程:

  1. 对key与val的NPE异常判断------->【有个N物品】;
  2. 计算key的hashcode------->【N物品打上在A盒子的位置坐标】;
  3. 进入tab无限循环中;
  4. tab为空的情况下,开始初始化tab,并且是以默认容量16进行,具体里面的逻辑也到扩容在讲------->【造A盒子】;
  5. 根据(n - 1) & hash计算在tab的下标,如下标为空,利用cas放置,跳出循环。
  6. 如果tab对应下标不为空,且该Node的hashcode为-1,那么判断该tab正在扩容。此时当前线程就得协助去扩容,helpTransfer也到扩容在讲;
  7. 如果tab对应下标不为空并且也非没有扩容标志,那么锁住当前链表或者数,保证了同时只有一个线程修改链表,防止出现链表成环。
  8. 冲突的这列的第一位node的hash不为负数,进行链表操作,查看hash以及key是否相等,是替换,反正添加在其链表尾部; 以上不满足则视为红黑树结构,进行数相关put操作
  9. 链表的会通过binCount记录链表长度,如果超过8,那么开始转数操作treeifyBin,这个方法在转数前,还会对总体容量是否达到64进行判断,不满足,则先扩容,满足才开始转数。
  10. 最后才是进行容器阀值的判断,也就是总容量的0.75.,不满足扩容,满足则put方法结束。

相信看完整体流程会发现与HashMap很类似,那么在这里我们看这几个重点方法:

initTable------tab初始化

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; //声明node数组
    int sc;          //声明sizeCtl控制标识

    //当table = 空进入
    while ((tab = table) == null || tab.length == 0) {

        //sizeCtl<0 说明有其他线程正在初始化,把线程挂起来. 对于tables初始化,只能有一个线程进行
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin

        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            //利用cas将sizeCtl变成-1,表示线程正要进行初始化.  在此之前sizeCtl可能为 0 为正数

            try {
                if ((tab = table) == null || tab.length == 0) {

                    //如果当前sizeCtl 大于 0 那么容量为sizeCtl , 否则使用默认容量即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 >>> 2); //相当于0.75*n ,计算下次扩容的阀值
                }
            } finally {
                sizeCtl = sc;//设置初始化后,下一次扩容阀值
            }
            break;
        }
    }
    return tab;
}

划重点:

  1. 当构建除ConcurrentHashMap(Map<? extends K, ? extends V> m)构建方法时,那么tab都是未初始化,都是会等到put、computeIfAbsent、compute、merge等方法的时候,才会进行实际吃实话,调用时机是检查table==null;
  2. 回答**“为什么在没有指定容量的时,默认容量时16呢?”**, 这个问题,是在没看源码之前才会有16这个数字疑问,但实际问题应该是 为什么容量都是的2的次幂呢?
    答:因为这个公式 : (n - 1) & hash , 前面我们有提过,这个公式相当于取余, 那么取余也就单纯为了得到在tab的取模公式, 而且用位运算效率是高于 hash % n这种方式。 好,原因很简单,我们来证明,这个公式与容量时2次幂有啥关系呢?--------------请看盘者另一篇文章JUC1.8-ConcurrentHashMap源码学习-容量是2次幂,专门说明;

helpTransfer------tab协助扩容

可以看下https://www.cnblogs.com/stateis0/p/9062085.html 博客,写的非常详细;

总结

整体来说,咱们的并发map的put操作,不能说全懂,也应该有所窥视了。
与1.7的最大区别,就是锁的颗粒更新小了,以及最好性能的扩容,还能进行协助;

那么在来解答下前面有几个问题:
为什么要用链表?
答: 为解决key的hashcode碰撞后, 将碰撞的值,用链表的方式储存,俗称拉链法;

为什么有了链表后,还需要红黑树?
答: 因为链表天生不是适合遍历查询, 但当一定数据量后,链表长度也会N长,新添加值还要一一与链表的值进行核对后,在放置链表尾部。 所以此时一定得要一个高效查询的数据结构存在,so这是用空间换时间的做法;

为什么链表长度达到8,才转红黑树?
答: 其实这个问题,源码注释已经给了答案:
在这里插入图片描述
简单来说,hashcode在理想的情况下,在桶中的分布是遵循泊松有图可看出,桶长度的k的变化规则,桶超过8的概率很小,因此这个8也是遵循非常严谨的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值