• ConcurrentHashMap 原理分析


    1 为什么有ConcurrentHashMap 
    hashmap是非线程安全的,hashtable是线程安全的,但是所有的写和读方法都有synchronized,所以同一时间只有一个线程可以持有对象,多线程情况下锁竞争会比较激烈,严重影响性能。基于这种情况,Doug Lee大师写了一个ConcurrentHashMap类。ConcurrentHashMap是对多线程各种特性深刻理解的经典范例,学习多线程编程不得不学ConcurrentHashMap。 

    2 特性 
    ConcurrentHashMap通过锁拆分机制,降低了锁的争用,写时加锁,读时不加锁,降低了锁的持有时间,所以ConcurrentHashMap在高并发情况下的性能得到了大幅提升,ConcurrentHashMap非常适用于读多写少的场景中。 

    3 原理 
    3.1 锁拆分 
    ConcurrentHashMap引入了Segment,通过将键值做hash,数据可以均匀的分布到每个Segment中,每次put,remove等操作的时候,锁的都是当前的Segment,这样就减少了锁的争用。 
    4 整体类图和关键数据结构 
    4.1 类图 

     


    4.2 数据存储 



    代码片段1: 
    ConcurrentHashMap: 
    final int segmentMask; 
    final int segmentShift; 
    final Segment<K,V>[] segments; 

    代码片段2: 
    SegMent: 
    transient volatile int count; 
    transient int modCount; 
    transient int threshold; 
    transient volatile HashEntry<K,V>[] table; 
    final float loadFactor; 

    代码片段3: 
    HashEntry: 
      final K key; 
      final int hash; 
      volatile V value; 
      final HashEntry<K,V> next; 

    由上可见,ConcurrentHashMap由Segment数组组成,Segment由table数组组成,每一个table元素都是一个由HashEntry组成的链表结构,hash冲突时会存放到同一个table的链表结构中。键值对保存在HashEntry对象中。 


    依次插入A B C后,Segment结构示意图: 




    4.3 Segment特性 
    transient volatile HashEntry<K,V>[] table; 

    是volatile的,避免了读取时加锁,volatile特性约束变量的值在本地线程副本中修改后会立即同步到主线程中,保证了其他线程的可见性。 
    4.3 HashEntry 
    final K key; 
      final int hash; 
      volatile V value; 
      final HashEntry<K,V> next; 

    除value外,其他的属性都是final的,value是volatile类型的,都修饰为final表明不允许在此链表结构的中间或者尾部做添加删除操作,每次只允许操作链表的头部。删除元素后,删除元素之后的链表保持不变,删除元素之前的链表重新复制一份,并指向删除元素之后的元素。 

    例如删除C元素: 



    注意删除之后原来元素的顺序反转了。 

    5 关键点: 
    5.1 put 
    ConcurrentHashMap: 
    public V put(K key, V value) { 
            //不允许value为空 
            if (value == null) 
                throw new NullPointerException(); 
            int hash = hash(key.hashCode()); 
    //通过segmentFor(hash)找到找到数据所在的segment 
    //调用Segment的put方法完成put操作 
            return segmentFor(hash).put(key, hash, value, false); 
        } 


    Segment: 
    V put(K key, int hash, V value, boolean onlyIfAbsent) { 
             //put操作需要先获取锁 
                lock(); 
                try { 
                    int c = count; 
    //超出界限,进行rehash,table容量扩充1倍。 
                    if (c++ > threshold) // ensure capacity 
                        rehash(); 
    //找到HashEntry的头               
    HashEntry<K,V>[] tab = table; 
                    int index = hash & (tab.length - 1); 
                    HashEntry<K,V> first = tab[index]; 
                    HashEntry<K,V> e = first; 
    //遍历查找key值是否已经存在 
                    while (e != null && (e.hash != hash || !key.equals(e.key))) 
                        e = e.next; 

                    V oldValue; 

                    if (e != null) {//如果已经存在,则直接替换value 
                        oldValue = e.value; 
                        if (!onlyIfAbsent) 
                            e.value = value; 
                    } 
                    else {//如果不存在,则插入到表头 
                        oldValue = null; 
                        ++modCount;//用于记录链表结构化调整,跨段求size会用到 
                        tab[index] = new HashEntry<K,V>(key, hash, first, value); 
                        count = c; // write-volatile 
                    } 
                    return oldValue; 
                } finally { 
                    unlock(); 
                } 
            } 


    5.2 get 
    V get(Object key, int hash) { 
                if (count != 0) { // read-volatile 
    //获取hashentry的头 
                    HashEntry<K,V> e = getFirst(hash); 
                    while (e != null) {//遍历 
                        if (e.hash == hash && key.equals(e.key)) { 
                            V v = e.value; 
                            if (v != null) 
                                return v; 
    //因为put的value不允许为空,所以如果值为空,说明有其他线程正在构造hashentry对象,发生了指令重排序,所以加锁重新读取一次。 
                            return readValueUnderLock(e); // recheck 
                        } 
                        e = e.next; 
                    } 
                } 
                return null; 
            } 

    5.3 size 
    final Segment<K,V>[] segments = this.segments; 
            long sum = 0; 
            long check = 0; 
            int[] mc = new int[segments.length]; 
            // Try a few times to get accurate count. On failure due to 
            // continuous async changes in table, resort to locking. 
            for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) { 
                check = 0; 
                sum = 0; 
                int mcsum = 0; 
    //遍历,并记录下每个Segment的modCount值 
                for (int i = 0; i < segments.length; ++i) { 
                    sum += segments[i].count; 
                    mcsum += mc[i] = segments[i].modCount; 
                } 
                if (mcsum != 0) { 
    //再遍历一次,看2次是否相同,如果不相同则再试一次,如果相同则返回size. 
                    for (int i = 0; i < segments.length; ++i) { 
                        check += segments[i].count; 
                        if (mc[i] != segments[i].modCount) { 
                            check = -1; // force retry 
                            break; 
                        } 
                    } 
                } 
                if (check == sum) 
                    break; 
            } 
    //尝试2次后,如果仍然不相等,则加锁重新读一遍。 
            if (check != sum) { // Resort to locking all segments 
                sum = 0; 
                for (int i = 0; i < segments.length; ++i) 
                    segments[i].lock(); 
                for (int i = 0; i < segments.length; ++i) 
                    sum += segments[i].count; 
                for (int i = 0; i < segments.length; ++i) 
                    segments[i].unlock(); 
            } 
            if (sum > Integer.MAX_VALUE) 
                return Integer.MAX_VALUE; 
            else 
                return (int)sum; 


    6. 思考 
    6.1 为什么查询可以不加锁? 
    1)通过HashEntry的不变性降低读操作加锁的需求。 

    HashEntry的属性key,next,hash都是final类型的,保证只能在头部修改链表,另外value设置为了volatile,保证了写线程写入后,其他读线程都可以看到新值。 

    非结构化修改:对于非结构化修改,因为value是volatile类型的,所以写线程修改后,读线程立刻可以看到修改后的值。 
    结构化修改:a)put,由于put插入到链表的表头,链表中的原有节点并没有改变,所以读线程可以正常遍历原有的链表 
    b)remove ,参见4.3中的图,原有链表也继续保留,所以读线程可以正常遍历链表。 

    2)用volatile变量协调读写线程的可见性 



    假设线程M写入count后,线程N读取count。 
    根据happen-before法则,A happen-before B, C happen-before D, 又根据volatile法则,B happen-bofere C,所以根据传递规则A happen-before D。 
    从get的代码中看,get会首先读取count,所以读线程能够看到之前对链表做的修改。 

    6.2 什么时候会造成数据不一致? 
    线程A先做put操作,线程B后做get操作。 

    假设put执行到红色注释处,切换到线程B则读到的是线程A put之前的操作,这个概率比较小,并且是允许的,如果要保证严格的一致性,那么只有给读操作加锁。这也印证了每种技术都有其适用的场景那句话,ConcurrentHashMap适用在读多写少的场景下。 
    V put(K key, int hash, V value, boolean onlyIfAbsent) { 
                  lock(); 
                try { 
                    int c = count; 
                    if (c++ > threshold) // ensure capacity 
                        rehash(); 
    HashEntry<K,V>[] tab = table; 
                    int index = hash & (tab.length - 1); 
                    HashEntry<K,V> first = tab[index]; 
                    HashEntry<K,V> e = first; 
                    while (e != null && (e.hash != hash || !key.equals(e.key))) 
                        e = e.next; 
    //-------------put执行到此处----------- 
                    V oldValue; 

                    if (e != null) { 
                        oldValue = e.value; 
                        if (!onlyIfAbsent) 
                            e.value = value; 
                    } 
                    else { 
                        oldValue = null; 
                        ++modCount; 
                        tab[index] = new HashEntry<K,V>(key, hash, first, value); 
                        count = c; // write-volatile 
                    } 
                    return oldValue; 
                } finally { 
                    unlock(); 
                } 
            } 

    7 参考资料: 
    http://blog.csdn.net/ykdsg/article/details/6257449 
    http://bhdweb.iteye.com/blog/1722431 
    http://bhdweb.iteye.com/blog/1722432 
    http://www.360doc.com/content/12/1105/20/9462341_246041701.shtml 
    http://www.gznote.com/2014/04/concurrenthashmap%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html     * 
    http://www.iteye.com/topic/344876 
    http://andy136566.iteye.com/blog/1070493 

  • 相关阅读:
    高阶函数与匿名函数
    A task in a suit and a tie:paraphrase generation with semantic augmentation解读
    利用tensorboard将数据可视化(tf1.x 和 tf2.x)
    IplImage, CvMat, Mat 的关系
    剑指OFFER之合并两个排序的链表
    整数与字符串的互相转化
    二分查找法
    集成算法
    003-决策树案例
    002-决策树构造实例
  • 原文地址:https://www.cnblogs.com/huajiezh/p/5790905.html
Copyright © 2020-2023  润新知