图解ConcurrentHashMap1.8源码

1.8的ConcurrentHashMap在线程安全方面做的设计主要是通过CAS + Synchronized 来保证并发操作的。数据结构和HashMap一致,但是在设计方面会有细节的区别, 首先从Node类开始参考,我会尽量找出一些和HashMap不同的关键点来做分析有助于帮助理解线程安全的设计思想。

另外补充一个CAS的开销问题https://www.jianshu.com/p/21be831e851e 参考

// 对Node当中的val 和 next成员变量增加了volatile来保证可见性
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
}

然后开始参考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循环是因为需要使用CAS机制来操作元素
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
/* hashMap如果发现tab[i = (n - 1 & hash)] 这个值 == null就直接new 一个节点放到这个数组下标i中
 在这里是用的tabAt方法,点开发现是Unsafe的getObjectVolatile方法,也很好理解,它保证线程拿到的是table中的最新元素。*/
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
/* 在下面显然如果要保证线程安全 必须要保证对元素赋值的操作的原子性,出于性能考虑使用了CAS操作。

compareAndSwapObject方法包含当前对象var1,偏移量var2,期望值var4,var5新值

如果当前对象对应偏移量的值与期望值相同,则把当前对象的值更改为新值,并返回true;否则返回false,

这里翻译一下就是首先利用volatile的可见性来查看最新的这个数组下标的节点是空的,再然后就需要把新

的节点放到这个数组下标, 所以我的var4应该是null作为期望值, 如果不是null了说明被其他线程操作
赋值了。*/
                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)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
// 发生hash冲突时的情况
                synchronized (f) {
/* 判断f是否是头结点(往回看有个f = tabAt操作已经对f赋值了, 如果头结点这时已不是f了那往下的操作

也不需要进行了,看源码的时候发现写代码有个特点就是判断和赋值的代码往往在同一行, 需要仔细观看比较费

劲) */
                    if (tabAt(tab, i) == f) {
/* fh >= 0 代表的是链表,看一下fh的计算, 再看下spread代码为什么要多一个&操作,就是因为 >0的时候代表的是链表做区分 */
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
/* if条件简单的来说就是要在链表当中找到对应键的这个节点, 没找到的话就会执行 pred 那里往下的逻辑

代码, 找到了的话在下面的if语句中就已经break了 */
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                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) {
                            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) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
/* CAS来更新ConcurrentHashMap的size, 总结下来看,addCount 方法做了 2 件事情:

对 table 的长度加一

检查是否需要扩容,或者是否正在扩容。如果需要扩容,就调用扩容方法,如果正在扩容,就帮助其扩容。*/

        addCount(1L, binCount);
        return null;
    }
/* 扩容机制 首先需要了解sizeCtl、transferIndex、ForwardingNode、bound、Advance、finishing一系列变量的意义
sizeCtl:多线程之间通过volatile来读取这个变量判断ConcurrentHashMap所处的状态,通过cas设置它的值
未初始化:
sizeCtl = 0 表示沒有指定初始容量 sizeCtl > 0 表示初始容量
初始化中:
sizeCtl = -1 告知其他线程正在初始化
正常状态:
sizeCtl = 0.75n, 扩容阈值
扩容中:
sizeCtl < 0 :表示有其他线程正在执行扩容
sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2:表示此时只有一个线程在执行扩容。
transferIndex:扩容的索引,迁移的上界,各个线程通过CAS设置transferIndex = transferIndex - stride来获取他们的迁移任务的区间(transferIndex ~ bound之间。 从后往前开始迁移)
ForwardingNode:用來标记哪个哈希桶迁移完成.
bound:迁移的下界
Advance:该参数指的是是否继续递减转移下一个桶,如果为 true,表示可以继续向后推进,反之,说明还没有处理好当前桶,不能推进
finishing:true代表完成了本次扩容,false代表没有. */

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
// 每个线程最少拥有16个迁移任务(迁移任务对应哈希桶,每个桶在下面迁移时都会被Synchronize锁住。)
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
/* 看这里的循环代码时会有疑惑, 换个思路去思考这行代码,根据我的理解作者本意只是在这行初始化定义

了两个变量, 以及设置了一个死循环。 i 和 bound的关系在死循环里才给出. */
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
/* 第一次循环这个if条件肯定是不通过的,后面的情况就是每次while循环执行完一次就是说上一个迁移任务完成了 i往前推一格, 

然后设置advance为false 证明现在i所处的索引的这个迁移任务还没有完成。 */
                if (--i >= bound || finishing)
                    advance = false;
/* nextIndex完成赋值 这一步的操作也就是用来理解一个关键的思路:当一个线程进入时会获取最新的当前迁

移任务的下标,如果小于0,就说明已经迁移完成了.i = - 1, advance = false 表明不再推进了. 加深理解

这行代码的含义就是说如果一个线程完成了自己的迁移区间后还有剩余的迁移区间没有别的线程处理,那么它就

会过来处理, 这行代码需要了解的上下文环境就是transferIndex在哪里做了修改, 其实就在下面ans 开头

注释的代码那里  */
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
/* ans :  在这里通过CAS操作 修改了ConcurrenHashMap的transferIndex这个变量所处的内存地址内的

值,通过这个操作来理解上面的注释就比较清晰了。 */
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
// i < 0 对应上面 i = -1的赋值情况代表扩容完成了不再推进
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
// 这里才是ForwardingNode标记迁移完成的地方。 回到最上面的注释查看相关作用
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
// 上锁是为了防止putVal来插值, 扩容和插入不能同时进行,
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
// 链表的情况 在下面开始真正的根据高低位来迁移节点到新的数组当中, 具体细节确实有些地方看不懂。
                        if (fh >= 0) {
                            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);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

https://www.jianshu.com/p/2829fe36a8dd扩容机制参考.

觉得博客好看的点个赞把!

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值