ConcurrentHashMap源码解析 5.get() & remove() 方法

ConcurrentHashMap—get() & remove() 方法

get()

  • get()方法:获取元素,根据目标 key 所在桶的第一个元素的不同采用不同的方式获取元素,关键点在于 find() 方法的重写。

流程总结

  • 计算 key 的 hash 值寻址到指定桶位。
  • 当前桶位没有元素直接返回 null。
  • 当前桶位如果是 fwd 节点或者是红黑树节点,则调用各自的find()方法。
  • 当前桶位不是 fwd 节点也不是红黑树节点,则说明当前为链表,遍历桶位,进行查找。
  • 查找元素没有任何的加锁操作。

源码解析

     /*
      * 根据key进行查找获取value值
      */     
     public V get(Object key) {
        /*
         * tab 引用table
         * e   当前元素
         * p   目标元素
         * n   table数组长度
         * eh  当前元素hash
         * ek  当前元素key
         */
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        // 扰动运算后得到 更散列的hash值 
        int h = spread(key.hashCode());
        /*
         * 进入if语句内部的条件:
         * 条件1:(tab = table) != null
         *   true->表示已经put过数据,并且map内部的table也已经初始化完毕
         *   false->表示创建完map后,并没有put过数据,map内部的table是延迟初始化的,只有第一次写数据时会触发创建逻辑
         * 条件2:(n = tab.length) > 0 true->表示table已经初始化
         * 条件3:(e = tabAt(tab, (n - 1) & h)) != null
         *   true->当前key寻址的桶位 有值
         *   false->当前key寻址的桶位中是null,是null直接返回null
         */
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            // 前置条件:当前桶位有数据
            /*
             * 对比与头结点的key是否完全一致
             *   1.比较key的hash值是否一致8
             *   2.比较key的内存地址是否一致 或 进行equals的返回true
             * 同时满足这两个条件才能认为key完全一致
             */
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    // 完全一致 直接返回value
                    return e.val;
            }
            /*
             * hash值小于0,有两种情况:
             *   1.hash = -1,表示当前桶位是FWD节点,表示当前桶位中的节点已经被迁移了
             *   2.hash = -2, 表示当前桶位是TreeBin节点
             * 如果是FWD节点的话,会进入对应的FWD中定义的find方法进行查找
             * 如果是红黑树会进入TreeNode内部的find方法进行查找(多态)
             */
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            // 当前桶位是一个链表,直接遍历每一个节点找到一个key完全相同的节点 返回value即可
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

ForwardingNode 的 find() 方法

上面在调用 get() 方法进行查找的时候,如果查找到当前桶位的节点的 hash 值为 -1,说明当前桶位的可能是 fwd 节点或者是红黑树节点,就要到指定的结构中去查找,这里我们分析一下当 hash 值为 -1 时,去 ForwardingNode 节点查找的方法,关于去红黑树中查找的源码解析,放到后续的 TreeBin 的源码解析中讲解。

流程总结

  • 直接去 nextTable 中查询数据,经过寻址算法后发现当前桶位没有元素。
    • 可能原桶位中的元素都是经过扩容后都是高 (低) 位,而当前寻址得到的桶位刚好相反。
  • 如果发现当前桶位还是 fwd 节点 (可能高并发下又发生了扩容) 那么继续去新表查询。
  • 如果发现当前桶位是红黑树,那么调用红黑树的 find 方法去查询。
  • 否则当前是链表,遍历链表查询即可。
  • find()方法没有加锁过程
        Node<K,V> find(int h, Object k) {
            // 自旋
            // outer:最外层的循环标签
            /*
             * tab一定不为null
             * 整个ConcurrentHashMap源码中,只有一个地方实例化ForwardingNode,
             * 就是在transfer迁移数据方法中执行了:ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
             * 将新表赋值给tab
             */
            outer: for (Node<K,V>[] tab = nextTable;;) {
                /*
                * e 表示新表中寻址算法得到的桶位头结点
                * n 表示新表的长度 
                */
                Node<K,V> e; int n;
                /*
                 * 条件1 2 3 永远不成立
                 * 条件4:在新扩容表中 重新定位 hash 对应的头结点
                 *   true -> 1.在oldTable中 对应的桶位在迁移之前就是null
                 *           2.扩容完成后,有其它写线程,将此桶位设置为了null
                 */
                if (k == null || tab == null || (n = tab.length) == 0 ||
                    (e = tabAt(tab, (n - 1) & h)) == null)
                    return null; // 新表桶位没有数据 直接返回null
                /*
                 * 前置条件:扩容后的表,对应hash的桶位不是null,e为此桶位的头结点
                 * e可能是哪些类型?
                 *   1.node类型
                 *   2.TreeBin类型
                 *   3.FWD类型(高并发下扩容后再次触发扩容)
                 */
                // 自旋
                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;
                    /*
                     * eh < 0
                     *   1.可能是TreeBin节点
                     *   2.可能是FWD节点(新扩容后的表,可能在并发很大的情况下,再次扩容) 
                     */
                    if (eh < 0) {
                        // fwd节点 继续进行外层循环,继续在新表中查找
                        if (e instanceof ForwardingNode) {
                            // 在新的fwd节点中拿到更新的表
                            tab = ((ForwardingNode<K,V>)e).nextTable;
                            continue outer; // 从最外层循环开始执行
                        }
                        // 说明此桶位为TreeBin节点,使用TreeBin.find()查找红黑树中相应节点
                        else
                            return e.find(h, k);
                    }
                    // 此处桶位是链表,遍历到链表末尾也没有找到目标节点,返回null
                    if ((e = e.next) == null)
                        return null;
                }
            }
        }
    }

remove()

流程总结

  • remove 底层调用的是 replaceNode 方法,传入的后两个参数都是 null,表示当前的替换操作是删除。
  • 计算 hash 值,自旋去尝试删除。
  • 寻址后桶位上没有元素,返回 null。
  • 桶位上是链表,则遍历链表查找元素进行删除。
  • 桶位上是 fwd 节点,表示正在扩容,先尝试帮助扩容,然后继续循环尝试删除。
  • 桶位上形成了红黑树,遍历树查找元素,然后删除,删除后树中元素节点数较少,则退化为链表
  • 如果确实删除了元素 (非value替换),调用 addCount() 将元素个数 -1。
    • 如果没有删除元素,则返回 null。
  • 删除过程加锁(sycnhronized)锁定当前桶位的头结点。

源码解析

 public V remove(Object key) {
        //调用了替换节点的方法
        return replaceNode(key, null, null);
    }
  		    ||
		    ||
            \/    
   /*
    *  @param Object key -> 表示要替换的key
    *  @param V value -> 要替换的目标值(从remove方法调用的value为null,即将值替换为null,就是删除该节点)
    *  @param Object cv(compare value) -> 如果cv不为null,则需要既对比key还要对比cv,这两个参数都一致,才能替换目标值
    */
 	final V replaceNode(Object key, V value, Object cv) {
        // 计算key经过扰动运算的hash  
        int hash = spread(key.hashCode());
       
        // 自旋
        for (Node<K,V>[] tab = table;;) {
            /*
             * f  表示桶位头节点
             * n  表示table长度
             * i  表示hash寻址后命中的下标
             * fh 表示桶位头节点的hash值
             */
            Node<K,V> f; int n, i, fh;
            /*
             * CASE1:
             * 如果table为null,或者当前寻址后命中的桶位没有元素,直接break循环
             */
            if (tab == null || (n = tab.length) == 0 ||
                (f = tabAt(tab, i = (n - 1) & hash)) == null)
                break; 
            /*
             * CASE2:
             * 前置条件:当前桶位不是null
             * 当前桶位是fwd节点,说明当前table正在扩容中,由于当前是写操作,需要尝试帮助table扩容
             */
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            /*   
             * CASE3:
             * 前置条件:当前桶位不是null
             * 当前桶位是链表或者红黑树
             */
            else {
                // 保留替换之前的value
                V oldVal = null;
                // 校验标记:用于判断是否锁错对象,从而进入下一次的自旋
                boolean validated = false;
                // 锁定当前桶位的头结点(分段锁思想)
                synchronized (f) {
                    // 判断sync锁定的对象是否是当前桶位头节点,防止当前线程加锁之前,其他线程修改过桶位的头节点
                    if (tabAt(tab, i) == f) {  
                        // 哈希值大于0,说明桶位为正常的Node节点(或者Node链表)
                        if (fh >= 0) {
                            // validated赋值为true
                            validated = true;
                            
                            // 自旋
                            /*
                             * e 表示当前循环的元素
                             * pred 表示上一个元素 
                             */
                            for (Node<K,V> e = f, pred = null;;) {
                                // 当前桶位的key
                                K ek;
                                // 找到了指定key
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    // 将找到的指定节点的val赋值给ev
                                    V ev = e.val;
                                    /*
                                     * cv == null -> true 表示这是一个删除操作
                                     * cv == ev -> true 表示这是一个替换操作
                                     */
                                    if (cv == null || cv == ev ||
                                        (ev != null && cv.equals(ev))) {
                                        // 进行删除 或者 替换操作
                                        
                                        // 将当前节点的值 赋值给 oldVal 后续返回会用到
                                        oldVal = ev;
                                        // 条件成立:说明当前是一个替换操作
                                        if (value != null)
                                            // 直接将value替换
                                            e.val = value;
                                        // 当前节点不是头结点,并且这是一次删除操作
                                        else if (pred != null)
                                        	// 直接在链表中将e删除即可
                                            pred.next = e.next;
                                        // 说明e是头结点
                                        else
                                            // 调用setTab方法将当前桶位设置为头结点的下一个节点(e.next)即可,变向的将e删除
                                            setTabAt(tab, i, e.next);
                                    }
                                    break;
                                }
                                // 向后遍历
                                pred = e;
                                // 遍历到链表尾部还没找到元素,跳出循环
                                if ((e = e.next) == null)
                                    break;
                            }
                        }
                        
                        // 这里是红黑树TreeBin节点
                        else if (f instanceof TreeBin) {
                            // 修改标志位
                            validated = true;
                            // 转换为指定的TreeBin节点
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            /* 
                             * r 表示红黑树根节点
                             * p 表示红黑树中查找到的对应的key一致的node
                             */
                            TreeNode<K,V> r, p;
                            /*
                            * 条件1:(r = t.root) != null 理论上是成立 
                            * (TreeBin一共有两个引用:1.root 引用红黑树结构 2.first 引用链表结构)
                        	* 条件2:TreeNode.findTreeNode 以当前节点为入口,向下查找key(包括本身节点)
                        	*        true->说明查找到相应key 对应的node节点,会赋值给p
                        	*/
                            if ((r = t.root) != null &&
                                // 在红黑树中找到了指定的节点
                              (p = r.findTreeNode(hash, key, null)) != null) {
                                // 保存p.val到pv
                                V pv = p.val;
                                // 条件1:cv == null 成立:不比对value,就做替换或者删除操作
                            	// 条件2:cv == pv ||(pv != null && cv.equals(pv)) 成立:说明“对比值”与当前p节点的值 一致
                                if (cv == null || cv == pv ||
                                    (pv != null && cv.equals(pv))) {
                                    // 替换或删除操作
                                    
                                    // 原值赋给oldVal
                                    oldVal = pv;  
                                    // value不为null,替换
                                    if (value != null)
                                        p.val = value;
                                    // 删除
                                    else if (t.removeTreeNode(p))          
                                     /* 
                                      * t.removeTreeNode(p)这个方法返回true表示删除节点后树的元素个数是否较少(boolean) 
                                      * 如果删除后树的元素个数较少则退化成链表
                                      */
                                     setTabAt(tab, i, untreeify(t.first));
                                }
                            }
                        }
                    }
                }
                /*
                 * 当其他线程修改过桶位的头节点时,说明当前线程锁错了对象
                 * 此时validated为false,不会进入到下面的if的逻辑中终止,则会进入下次自旋
                 * 
                 * 如果此时validated为true,说明进入过锁的逻辑,并执行过删除/替换操作
                 * 然后判断是否真正的删除了节点,是的话则调用addCount()将元素数量-1,然后结束方法
                 */
                if (validated) {
                    // oldVal不为null,说明操作成功
                    if (oldVal != null) {
                        // value == null 说明这是一次删除操作,更新节点个数(-1)
                        if (value == null)
                            addCount(-1L, -1);
                        // 返回旧值
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null;
    }

参考

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小成同学_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值