美文网首页
ConcurrentMap

ConcurrentMap

作者: 一凡呀 | 来源:发表于2018-02-02 15:13 被阅读0次

    JDK1.7

    数据结构

    jdk1.7中采用了数组加链表的方式进行实现,如下图


    image.png

    concurrentMap在执行初始化时,计算segment数组的大小ssize和每个segment中HashEntry数组的大小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的方法来保证并发安全进行


    image.png

    只有在执行第一次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.判空;ConcurrentHashMap的key、value都不允许为null
    2.计算hash。利用方法计算hash值。
    3.遍历table,进行节点插入操作,过程如下:
    如果table为空,则表示ConcurrentHashMap还没有初始化,则进行初始化操作:initTable()
    根据hash值获取节点的位置i,若该位置为空,则直接插入,这个过程是不需要加锁的。计算f位置:i=(n - 1) & hash
    如果检测到fh = f.hash == -1,则f是ForwardingNode节点,表示有其他线程正在进行扩容操作,则帮助线程一起进行扩容操作
    如果f.hash >= 0 表示是链表结构,则遍历链表,如果存在当前key节点则替换value,否则插入到链表尾部。如果f是TreeBin类型节点,则按照红黑树的方法更新或者增加节点
    若链表长度 > TREEIFY_THRESHOLD(默认是8),则将链表转换为红黑树结构
    调用addCount方法,ConcurrentHashMap的size + 1
    这里整个put操作已经完成。

    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实现

    为每个内核分任务,并保证其不小于16
    检查nextTable是否为null,如果是,则初始化nextTable,使其容量为table的两倍
    死循环遍历节点,直到finished:节点从table复制到nextTable中,支持并发,请思路如下:
    如果节点 f 为null,则插入ForwardingNode(采用Unsafe.compareAndSwapObjectf方法实现),这个是触发并发扩容的关键
    如果f为链表的头节点(fh >= 0),则先构造一个反序链表,然后把他们分别放在nextTable的i和i + n位置,并将ForwardingNode 插入原节点位置,代表已经处理过了
    如果f为TreeBin节点,同样也是构造一个反序 ,同时需要判断是否需要进行unTreeify()操作,并把处理的结果分别插入到nextTable的i 和i+nw位置,并插入ForwardingNode 节点
    所有节点复制完成后,则将table指向nextTable,同时更新sizeCtl = nextTable的0.75倍,完成扩容过程
    在多线程环境下,ConcurrentHashMap用两点来保证正确性:ForwardingNode和synchronized。当一个线程遍历到的节点如果是ForwardingNode,则继续往后遍历,如果不是,则将该节点加锁,防止其他线程进入,完成后设置ForwardingNode节点,以便要其他线程可以看到该节点已经处理过了,如此交叉进行,高效而又安全。


    image.png

    本文参考 链接:
    https://www.jianshu.com/p/e694f1e868ec
    http://blog.csdn.net/u010412719/article/details/52145145

    相关文章

      网友评论

          本文标题:ConcurrentMap

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