【JDK源码分析系列】ConcurrentHashMap 源码分析 -- 增、删、查操作

【JDK源码分析系列】ConcurrentHashMap 源码分析 -- 增、删、查操作

【0】ConcurrentHashMap 整体架构

【JDK源码分析系列】ConcurrentHashMap 源码分析 -- 增、删、查操作_第1张图片

构成说明
采用transient volatile HashEntry[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,并发控制使用Synchronized和CAS来操作
采用table数组+单向链表+红黑树的结构

ConcurrentHashMap 和 HashMap 比较

相同之处
1. 数组、链表结构几乎相同,所以底层对数据结构的操作思路是相同的
2. 都实现了 Map 接口,继承了 AbstractMap 抽象类,所以大多数的方法也都是相同的,
     HashMap 有的方法,ConcurrentHashMap 几乎都有,
     当需要从 HashMap 切换到 ConcurrentHashMap 时,无需关心两者之间的兼容问题
不同之处
1. 红黑树结构略有不同,
   HashMap 的红黑树中的节点叫做 TreeNode,TreeNode 不仅仅有属性,还维护着红黑树的结构,比如说查找,新增等等;
   ConcurrentHashMap 中红黑树被拆分成两块,TreeNode 仅仅维护的属性和查找功能,
   新增了 TreeBin,来维护红黑树结构,并负责根节点的加锁和解锁
2. 新增 ForwardingNode (转移)节点,扩容的时候会使用到,通过使用该节点,来保证扩容时的线程安全

【1】ConcurrentHashMap 新增数据

/**
 * 单纯的额调用putVal方法,并且putVal的第三个参数设置为false
 * 当设置为false的时候表示这个value一定会设置
 * 当设置为true的时候,只有当这个key的value为空的时候才会设置
 */
public V put(K key, V value) {
    return putVal(key, value, false);
}

//1:如果数组为空,初始化,
//2:计算当前槽点有没有值,没有值的话,cas 创建,失败继续自旋
//3:如果槽点是转发节点(正在扩容),就会一直自旋等待扩容完成之后再新增
//4:槽点有值的,先锁定当前槽点,其余槽点不能操作
//4.1 : 如果是链表,新增值到链表的尾部
//4.2 : 如果是红黑树,在红黑树着色旋转的时候,会考虑把红黑树的根节点锁住
//5:check需不需要扩容,需要的话去扩容.
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // key value 不能为空
    if (key == null || value == null) throw new NullPointerException();
    //计算 hash
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node[] tab = table;;) {
        Node f; int n, i, fh;
        //table 是空的,初始化
        if (tab == null || (n = tab.length) == 0)
            // 处理初始化,并发的情况在initTable中处理,这里不考虑
            tab = initTable();
        //如果当前索引位置没有值,直接创建
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //cas 在 i 位置创建新的元素,当i位置是空时,创建成功结束for自循环,否则继续自旋
            if (casTabAt(tab, i, null,
                            new Node(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //发现转发节点,表明此时正在进行扩容,去帮助扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        //槽点上有值的
        else {
            V oldVal = null;
            //锁定当前槽点,其余线程不能操作,保证了安全
            synchronized (f) {
                //这里再次判断 i 索引位置的数据没有被修改
                //binCount 被赋值的话,说明走到了修改表的过程里面
                //保证锁住的是hash桶的第一个节点,这样阻止其他写操作进入,
                //如果锁住的不是第一个节点,那么重新开始循环
                if (tabAt(tab, i) == f) {
                    //链表
                    if (fh >= 0) {
                        //因为第一个节点处理了,这里赋值为1
                        binCount = 1;
                        for (Node e = f;; ++binCount) {
                            K ek;
                            //找到“相等”的节点,看看是否需要更新value的值
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                    (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node pred = e;
                            //到链表最后一个节点的时候,都没有
                            //把新增的元素赋值到链表的最后
                            if ((e = e.next) == null) {
                                pred.next = new Node(hash, key,
                                                            value, null);
                                break;
                            }
                        }
                    }
                    //红黑树,这里没有使用 TreeNode,使用的是 TreeBin,TreeNode 只是红黑树的一个节点
                    //TreeBin 持有红黑树的引用,并且会对其加锁,保证其操作的线程安全
                    else if (f instanceof TreeBin) {
                        Node p;
                        //设置为2,保证addCount中能够进行扩容判断,
                        //同时也不会触发链表转化为红黑树的操作
                        binCount = 2;
                        //满足if的话,把老的值给oldVal
                        //在putTreeVal方法里面,在给红黑树重新着色旋转的时候
                        //会锁住红黑树的根节点
                        if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                        value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            //binCount不为空,并且 oldVal 有值的情况,说明已经新增成功了
            if (binCount != 0) {
                // 链表是否需要转化成红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    //链表转换为红黑树
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                //这一步几乎走不到,槽点已经上锁,只有在红黑树或者链表新增失败的时候
                //才会走到这里,这两者新增都是自旋的,几乎不会失败
                break;
            }
        }
    }
    //check 容器是否需要扩容,如果需要去扩容,调用 transfer 方法去扩容
    //如果已经在扩容中了,check完成
    addCount(1L, binCount);
    return null;
}

 

【2】ConcurrentHashMap 查找数据流程

public V get(Object key) {
    Node[] tab; Node e, p; int n, eh; K ek;
    //计算hashcode
    int h = spread(key.hashCode());
    //不是空的数组 && 并且当前索引的槽点数据不是空的
    //否则该key对应的值不存在,返回null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        //槽点第一个值和key相等,直接返回
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0)
        // hash桶的第一个节点的hash值小于0,代表它是特殊节点,使用特化的查找方式进行查找
        // ForwardingNode会把find转发到nextTable上再去执行一次
        // TreeBin则根据自身读写锁情况,判断是用红黑树方式查找,还是用链表方式查找
        // ReservationNode本身只是为了synchronized有加锁对象而创建的空的占位节点,
        // 因此本身hash桶是没节点的,一定找不到,直接返回null
            return (p = e.find(h, key)) != null ? p.val : null;
        //如果是链表,遍历查找
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

【3】ConcurrentHashMap 删除数据流程

public V remove(Object key) {
    return replaceNode(key, null, null);
}

// remove删除,可以看成是用null替代原来的节点
final V replaceNode(Object key, V value, Object cv) {
    int hash = spread(key.hashCode());
    for (Node[] tab = table;;) {
        Node f; int n, i, fh;
        // 没有节点,删除不了,直接退出
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;
        // 发现转发节点,表明此时正在进行扩容,去帮助扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            boolean validated = false;
            //这里跟put一样,尝试锁住hash桶的第一个结点,要保证锁住的是第一个结点
            synchronized (f) {
                //确保锁住的是第一个节点
                if (tabAt(tab, i) == f) {
                    //处理链表相关的删除/替换操作
                    if (fh >= 0) {
                        validated = true;
                        for (Node e = f, pred = null;;) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                    (ek != null && key.equals(ek)))) {
                                //确定了待删除的值
                                V ev = e.val;
                                if (cv == null || cv == ev ||
                                    (ev != null && cv.equals(ev))) {
                                    oldVal = ev;
                                    //value != null 表明执行的是替换操作
                                    if (value != null)
                                        e.val = value;
                                    //进行删除节点 e 的操作
                                    else if (pred != null)
                                        pred.next = e.next;
                                    //删除的是第一个节点,就重设第一个节点,此时相当于已经释放了锁
                                    else
                                        setTabAt(tab, i, e.next);
                                }
                                break;
                            }
                            pred = e;
                            if ((e = e.next) == null)
                                //判断循环遍历是否已经结束
                                break;
                        }
                    }
                    //处理红黑树相关的删除/替换操作
                    else if (f instanceof TreeBin) {
                        validated = true;
                        TreeBin t = (TreeBin)f;
                        TreeNode r, p;
                        //在红黑树中确定待删除/替换的节点
                        if ((r = t.root) != null &&
                            (p = r.findTreeNode(hash, key, null)) != null) {
                            V pv = p.val;
                            if (cv == null || cv == pv ||
                                (pv != null && cv.equals(pv))) {
                                oldVal = pv;
                                //value != null 表明进行替换操作
                                if (value != null)
                                    p.val = value;
                                //红黑树删除节点
                                //removeTreeNode : 红黑树的规模太小时,返回true
                                else if (t.removeTreeNode(p))
                                    //处理红黑树退化为链表的情况
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            // 下面这一段判断是否是删除操作,是删除操作就把计数值减1
            if (validated) {
                if (oldVal != null) {
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    return null;
}

致谢

本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。

【1】Java中Unsafe类详解

【2】ConcurrentHashMap的tabAt为什么不用tab[i]

【3】ConcurrentHashMap实现原理及源码分析

【4】Java集合类框架学习 5.3—— ConcurrentHashMap(JDK1.8)

你可能感兴趣的:(JDK,数据结构)