美文网首页工作相关java并发编程Java整理
谈谈ConcurrentHashMap1.7和1.8的不同实现

谈谈ConcurrentHashMap1.7和1.8的不同实现

作者: 美团Java | 来源:发表于2017-02-12 19:53 被阅读17204次

    简书 占小狼
    转载请注明原创出处,谢谢!

    知止而后有定,定而后能静,静而后能安,安而后能虑,虑而后能得。

    ConcurrentHashMap

    在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈建议使用ConcurrentHashMap代替HashMap,为了对更深入的了解,本文将对JDK1.7和1.8的不同实现进行分析。

    JDK1.7

    数据结构

    jdk1.7中采用Segment + HashEntry的方式进行实现,结构如下:

    ConcurrentHashMap初始化时,计算出Segment数组的大小ssize和每个SegmentHashEntry数组的大小cap,并初始化Segment数组的第一个元素;其中ssize大小为2的幂次方,默认为16,cap大小也是2的幂次方,最小值为2,最终结果根据根据初始化容量initialCapacity进行计算,计算过程如下:

    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
    

    其中Segment在实现上继承了ReentrantLock,这样就自带了锁的功能。

    put实现

    当执行put方法插入数据时,根据key的hash值,在Segment数组中找到相应的位置,如果相应位置的Segment还未初始化,则通过CAS进行赋值,接着执行Segment对象的put方法通过加锁机制插入数据,实现如下:

    场景:线程A和线程B同时执行相同Segment对象的put方法

    1、线程A执行tryLock()方法成功获取锁,则把HashEntry对象插入到相应的位置;
    2、线程B获取锁失败,则执行scanAndLockForPut()方法,在scanAndLockForPut方法中,会通过重复执行tryLock()方法尝试获取锁,在多处理器环境下,重复次数为64,单处理器重复次数为1,当执行tryLock()方法的次数超过上限时,则执行lock()方法挂起线程B;
    3、当线程A执行完插入操作时,会通过unlock()方法释放锁,接着唤醒线程B继续执行;

    size实现

    因为ConcurrentHashMap是可以并发插入数据的,所以在准确计算元素时存在一定的难度,一般的思路是统计每个Segment对象中的元素个数,然后进行累加,但是这种方式计算出来的结果并不一样的准确的,因为在计算后面几个Segment的元素个数时,已经计算过的Segment同时可能有数据的插入或则删除,在1.7的实现中,采用了如下方式:

    try {
        for (;;) {
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // force creation
            }
            sum = 0L;
            size = 0;
            overflow = false;
            for (int j = 0; j < segments.length; ++j) {
                Segment<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount;
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            if (sum == last)
                break;
            last = sum;
        }
    } finally {
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    

    先采用不加锁的方式,连续计算元素的个数,最多计算3次:
    1、如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;
    2、如果前后两次计算结果都不同,则给每个Segment进行加锁,再计算一次元素的个数;

    JDK1.8

    数据结构

    1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,结构如下:

    只有在执行第一次put方法时才会调用initTable()初始化Node数组,实现如下:

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        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);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
    

    put实现

    当执行put方法插入数据时,根据key的hash值,在Node数组中找到相应的位置,实现如下:

    1、如果相应位置的Node还未初始化,则通过CAS插入相应的数据;

    else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
            break;                   // no lock when adding to empty bin
    }
    

    2、如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;

    if (fh >= 0) {
        binCount = 1;
        for (Node<K,V> e = f;; ++binCount) {
            K ek;
            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;
            }
        }
    }
    

    3、如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;

    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;
        }
    }
    

    4、如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;

    if (binCount != 0) {
        if (binCount >= TREEIFY_THRESHOLD)
            treeifyBin(tab, i);
        if (oldVal != null)
            return oldVal;
        break;
    }   
    

    5、如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount

    size实现

    1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount,实现如下:

    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
    

    1、初始化时counterCells为空,在并发量很高时,如果存在两个线程同时执行CAS修改baseCount值,则失败的线程会继续执行方法体中的逻辑,使用CounterCell记录元素个数的变化;

    2、如果CounterCell数组counterCells为空,调用fullAddCount()方法进行初始化,并插入对应的记录数,通过CAS设置cellsBusy字段,只有设置成功的线程才能初始化CounterCell数组,实现如下:

    else if (cellsBusy == 0 && counterCells == as &&
             U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
        boolean init = false;
        try {                           // Initialize table
            if (counterCells == as) {
                CounterCell[] rs = new CounterCell[2];
                rs[h & 1] = new CounterCell(x);
                counterCells = rs;
                init = true;
            }
        } finally {
            cellsBusy = 0;
        }
        if (init)
            break;
    }
    

    3、如果通过CAS设置cellsBusy字段失败的话,则继续尝试通过CAS修改baseCount字段,如果修改baseCount字段成功的话,就退出循环,否则继续循环插入CounterCell对象;

    else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
        break; 
    

    所以在1.8中的size实现比1.7简单多,因为元素个数保存baseCount中,部分元素的变化个数保存在CounterCell数组中,实现如下:

    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }
    
    final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }
    

    通过累加baseCountCounterCell数组中的数量,即可得到元素的总个数;


    我是占小狼,如果读完觉得有收获的话,欢迎点赞加关注

    相关文章

      网友评论

      • 冉桓彬:请教个问题, 文章中画图是用的什么软件?
        美团Java:@冉桓彬 https://www.processon.com/i/5799c92ee4b0e645bc5a5b74
      • _BK_徐静:怎么理解basecount和cellsbusy
        一个是cas成功的,一个是cas失败的吗?
      • 6935bd02ed56:能力有限,看不懂,学习了
        美团Java:@lifeChallenges 加油学习啊
      • f76f9308d391:怎么说呢,我需要研究下ConcurrentHashMap的源码,你一篇文章讲了,我对你那篇文章有些质疑惑,你另外一篇文章又把我的疑惑解决了,我还需要研究下1.8之前的锁分段原理,你这篇文章又帮我解决了。。“无微不至”的干货啊,感谢小狼
        美团Java:@淡淡的时候 哈哈,感觉是针对你的需求写的
      • Misout:你画图的软件是用什么画的?
        美团Java:@Misout processon
      • MathiasLuo:那1.8的size会出现 ,可能会出现问题吗? 我们在遍历countcells的数量时,另一边的cas成功了呀?
        美团Java:@MathiasLuo 其实在高并发的情况下,计算元素个数没有意义,但是你得理解怎么才能保证在某一刻计算出来的是准确的
        MathiasLuo:@占小狼 还是没能理解 既然是并发。可能我对 countcell 还没搞得太明白。
        美团Java:@MathiasLuo 不会的,那一刻的数量是准确的
      • 4c727a907ea8:狼哥的讲解深入浅出,实用
        美团Java:@简了直了 :smile:
      • wcyong:非常好!
        美团Java:@wcyong 多谢支持啊
      • ExploringKing:手赞!
      • d43a8cf92910:膜拜大神
      • 窝牛狂奔:好文
        美团Java:@窝牛狂奔 :smile:
      • chenssy:好文

      本文标题:谈谈ConcurrentHashMap1.7和1.8的不同实现

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