美文网首页
ConcurrentHashMap源码导读之put()方法

ConcurrentHashMap源码导读之put()方法

作者: 犄角芝士 | 来源:发表于2018-11-08 18:05 被阅读0次

    一、简介

    put()能将对应的key与value保存到map中。在ConcurrentHashMap中,key与value都不能为空,否则会抛出NullPointerException异常。如果put()时,key已经存在,则会返回put()前该key对应的value。

    public static void main(String[] args) {
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
        map.put("key", "oldValue");
        System.out.println(map.put("key", "newValue"));
    }
    
    // 输出结果
    oldValue
    

    问题1:为什么ConcurrentHashMap中,key与value都不能为空?

    二、源码

    put()中只有一行代码,调用了putVal()方法,putVal()有三个参数,key、value与onlyIfAbsent。参考HashMap中的解释,"if true, don't change existing value",如果为true,put()时如果key已经存在,就不改变当前的value,反之如果为false,就会出现上面代码中的情况,替换并返回旧的value。

    1、 准备

    putVal()的开始先校验了key与value。

    if (key == null || value == null) throw new NullPointerException();
    

    然后求了key的hash值。object.hashCode()是native方法,作为key的常用类String中对hashCode()进行了重写。在hashCode()之后又使用了spread()方法进行处理,得到key的hash值。

    问题2:spread()具体干了什么?有什么意义?

    int hash = spread(key.hashCode());
    

    2、插入

    接下来就是最重要的插入部分了,一个for循环中包含了一个if..else..结构。

    如果空表时,则进行初始化操作

    if (tab == null || (n = tab.length) == 0)
        tab = initTable();
    

    问题3:initTable()的流程

    如果bin为空时,则进行CAS插入

    if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        if (casTabAt(tab, i, null,
                     new Node<K,V>(hash, key, value, null)))
            break;
    }
    

    如果有线程正在resize-扩容的过程中,则两个线程一起进行resize操作

    if ((fh = f.hash) == MOVED)
        tab = helpTransfer(tab, f);
        
    static final int MOVED  = -1;
    

    问题4:f.hash在什么时候被赋值为-1?

    根据helpTransfer()的注释"Helps transfer if a resize is in progress.",推断当前if条件是判断正在进行resize操作。

    问题5:helpTransfer()的流程

    同步块中进行插入操作,也就是hash碰撞时的解决方案

    Node<K,V> f;
    
    synchronized (f) {
        if (tabAt(tab, i) == f){
            ...
        }
    }
    

    虽然tabAt()赋值到f过,但是多线程环境下可能此时f已经改变,所以在同步块中再进行一次判断。

    • 链表中的插入

    f为当前bin中的第一个元素

    if (fh >= 0) {
        // 前面已经判断过了bin为空的情况,所以这里binCount最小为1
        binCount = 1;
        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;
            }
            // 查找e结点的下一个节点,没有则创建
            Node<K,V> pred = e;
            if ((e = e.next) == null) {
                pred.next = new Node<K,V>(hash, key, value, null);
                break;
            }
        }
    }
    
    • 红黑树中的插入

    当bin中数据结构为树时,根节点的hash值为-2

    static final int TREEBIN  = -2; // hash for roots of trees
    
    else if (f instanceof TreeBin) {
        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;
        }
    }
    

    问题6:putTreeVal()的流程及红黑树的原理

    插入后判断bin中node个数

    static final int TREEIFY_THRESHOLD = 8;
    
    if (binCount != 0) {
        // 如果node个数大于等于8则将其转为红黑树
        if (binCount >= TREEIFY_THRESHOLD)
            treeifyBin(tab, i);
        if (oldVal != null)
            return oldVal;
        break;
    }
    

    3、resize

    改变map中元素的个数,如果有必要则进行resize

    addCount(1L, binCount);
    

    问题7:addCount()的流程

    三、总结

    遗留的问题

    1. 为什么ConcurrentHashMap中,key与value都不能为空
    2. spread()具体干了什么?有什么意义?
    3. initTable()的流程
    4. f.hash在什么时候被赋值为-1?
    5. helpTransfer()的流程
    6. putTreeVal()的流程及红黑树的原理
    7. addCount()的流程

    相关文章

      网友评论

          本文标题:ConcurrentHashMap源码导读之put()方法

          本文链接:https://www.haomeiwen.com/subject/vezwxqtx.html