• ConcurrentHashMap源码分析


    1.ConcurrentHashMap原理介绍(JDK1.7)

    我们知道HashTable在并发情况下效率低的原因是所有访问HashTable的线程都必须竞争同一把锁,这就导致一旦一个线程获取了锁,其余所有的线程都必须处于等待状态。而ConcurrentHashMap使用的是锁分段技术,就是将数据分为一段一段的存储,给每一段数据都配一把锁,当一个线程占用一把锁访问其中一段数据的时候,其他段的数据也能够被其他线程访问。ConcurrentHashMap是由segment数组构成,segment是一个可重入锁,它的结构和HashMap类似,底层是一个HashEntry数组,看如下图:

    2.构造函数

     @SuppressWarnings("unchecked")
        public ConcurrentHashMap(int initialCapacity,
                                 float loadFactor, int concurrencyLevel) {
            if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
                throw new IllegalArgumentException();
            if (concurrencyLevel > MAX_SEGMENTS)
                concurrencyLevel = MAX_SEGMENTS;
            // Find power-of-two sizes best matching arguments
            int sshift = 0;
            int ssize = 1;
            while (ssize < concurrencyLevel) {
                ++sshift;
                ssize <<= 1;
            }
            this.segmentShift = 32 - sshift;
            this.segmentMask = ssize - 1;
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
    //initialCapacity是ConcurrentHashMap的初始化容量
    int c = initialCapacity / ssize; if (c * ssize < initialCapacity) ++c;
    // cap是每个segment的容量,也就是HashEntry数组的长度,它也是一个2的N次幂值
    int cap = MIN_SEGMENT_TABLE_CAPACITY; while (cap < c) cap <<= 1; // create segments and segments[0]
    // loadFactor是每个segment的负载因子,创建Segments数组,然后初始化每个Segment的值
    Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]); Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize]; UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0] this.segments = ss; }

    通过以上代码可知:Segment的数组长度ssize是由concurrencyLevel计算出来的,ssize是一个大于等于concurrencyLevel的最小的2的N次幂值。sshift等于ssize从1向左移位的次数,默认情况下会移动4次,即sshift等于4。segmentShift和segmentMask在定位segment的算法中有使用到。

    3 方法

    3.1 get方法

     public V get(Object key) {
            Segment<K,V> s; // manually integrate access methods to reduce overhead
            HashEntry<K,V>[] tab;
    // 根据key的hash值h,以及之前得到的segmentShift,segmentMask等值得到u,从而定位到segment
    int h = hash(key); long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) {
    // 在定位到Segment后,然后定位到HashEntry数组中的位置,最后通过逐一比较,返回要找的值
    for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) { K k; if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; }

    有两个问题值得思考:一是在上面代码中,使用了UNSAFE.getObjectVolatile(segments, u)来获取segment,为什么不直接使用segments[u]来获取呢?我们知道每个线程都有自己的工作内存,里面存放着segments的副本,在并发环境下,并不能保证每次拿到的是segments中的最新元素。但是使用UNSAFE.getObjectVolatile(segments, u)可以直接获取指定内存的数据,保证拿到的数据是最新的。二是读操作为什么不需要加锁?get方法的高效之处就是不加锁,我们知道HashTable读操作时需要加锁的,那么ConcurrentHashMap是怎么做到的呢?HashEntry中的value值被定义为了volatile类型,这样value就能在多个线程间保持可见性,能够被多个线程读,而不会读到过期的值。一旦有线程修改value值,那么其他线程本地内存中的value值就会失效,从而保证去获取最新的value值,这是volatile代替锁的经典应用场景。

    3.2 put方法

        @SuppressWarnings("unchecked")
        public V put(K key, V value) {
            Segment<K,V> s;
            if (value == null)
                throw new NullPointerException();
    // 先根据key,segmentShift,segmentMask定位到segment
    int hash = hash(key); int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment s = ensureSegment(j);
    // 然后,将key和value放到segment中 进入该方法
    return s.put(key, hash, value, false); }
     final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // put操作需要加锁 HashEntry
    <K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { HashEntry<K,V>[] tab = table; int index = (tab.length - 1) & hash; HashEntry<K,V> first = entryAt(tab, index); for (HashEntry<K,V> e = first;;) { if (e != null) { K k; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { oldValue = e.value; if (!onlyIfAbsent) { e.value = value; ++modCount; } break; } e = e.next; } else { if (node != null) node.setNext(first); else node = new HashEntry<K,V>(hash, key, value, first); int c = count + 1;
    // 满足条件,则进行扩容操作
    if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally {
    // 解锁 unlock(); }
    return oldValue; }

    在put方法中,需要注意的是扩容时,针对的是某个segment,而不是对整个容器扩容。

    3.3 size方法

        public int size() {
            // Try a few times to get accurate count. On failure due to
            // continuous async changes in table, resort to locking.
            final Segment<K,V>[] segments = this.segments;
            int size;
            boolean overflow; // true if size overflows 32 bits
            long sum;         // sum of modCounts
            long last = 0L;   // previous sum
            int retries = -1; // first iteration isn't retry
            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; } }
    // 如果在累加count前后,modCount不变,则跳出循环
    if (sum == last) break; last = sum; } } finally {
    // 尝试三次后,需要解锁
    if (retries > RETRIES_BEFORE_LOCK) { for (int j = 0; j < segments.length; ++j) segmentAt(segments, j).unlock(); } } return overflow ? Integer.MAX_VALUE : size; }

    对于获取ConcurrentHashMap中元素的个数,就是把每个segment中count的个数相加即可,但是这里有个问题,在多线程环境下,在累加count的过程中,之前已经累加过的count可能会发生变化,使用加锁处理的话能解决这个问题,但是在累加count操作过程中,之前累加过的count发生变化的几率非常小,那么ConcurrentHashMap是这样解决的:先尝试三次不加锁的方式来累加count,在累加结束前后比较modCount的值是否发生变化;如果发生变化,则再通过加锁的方式来统计累加count。

    最后关于ConcurrentHashMap,还有个问题:它的迭代器是强一致性的迭代器还是弱一致性的迭代器?在java.util包中集合类都返回fail-fast迭代器,这就意味着,在集合迭代过程中,如果修改集合内容,则会抛出异常ConcurrentModificationException,这是强一致性迭代器。但是在java.util.concurrent 集合返回的迭代器,是弱一致性迭代器,在遍历过程中,如果已经遍历的集合上的内容变化了,迭代器不会抛出ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap迭代器弱一致的表现,这是为了提升效率,是一致性与效率之间的一种权衡。

  • 相关阅读:
    virtual private catalog
    About Health Monitor Checks
    修改ORACLE-NLS_DATE_FORMAT时间格式的四种方式
    About Quick Packaging and Custom Packaging
    Fault Diagnosability Infrastructure Overview
    Basic Concepts of Block Media Recovery
    破解:前一刻觉得还有好多事要做,突然间就不知道该干什么了
    利用OSG实现模拟飞机尾迹-粒子系统
    解决电脑上PPT频繁刷新的问题
    如何查看自己电脑系统的安装日期-Window上
  • 原文地址:https://www.cnblogs.com/51life/p/10126942.html
Copyright © 2020-2023  润新知