ConcurrentHashMap1.7源码分析

  别人十一在外浪啊浪,我还是喜欢自己宅着学习看看书看看电影写写博客美滋滋(不过我觉得还是因为穷才这样自我安慰?)。
  ConcurrentHashMap的实现在1.7到1.8有了较大的变化,对此本文主要依据源码和网上查阅的相关博客对源码进行分析和总结。首先会介绍一些前提基础知识,然后对于源码部分主要是依据自己在看的过程中添加的注释方式进行分析,由于本次设计细节较多,可能会有部分内容个人理解有误,欢迎指正。

Unsafe类和内存屏障简介

  关于ConcurrentHashMap的实现,不论是在jdk1.7还是jdk1.8版本中ConcurrentHashMap中使用的最为核心也是最为频繁的就是Unsafe类中的各种native本地方法。所以这里有必要先介绍一下其中用的最多的几个Unsafe类中的核心方法。主要的几个方法是Unsafe.putObjectVolatile(obj,long,obj2)Unsafe.getObjectVolatileUnsafe.putOrderedObject等,具体这几个方法的区别通过对应的c的源代码可以略知一二:

void sun::misc::Unsafe::putObjectVolatile (jobject obj, jlong offset, jobject value)
  {
  write_barrier ();
  volatile jobject *addr = (jobject *) ((char *) obj + offset);
  *addr = value;
  }

void sun::misc::Unsafe::putObject (jobject obj, jlong offset, jobject value)
  {
  jobject *addr = (jobject *) ((char *) obj + offset);
  *addr = value;
  }//用于和putObjectVolatile进行对比

jobject sun::misc::Unsafe::getObjectVolatile (jobject obj, jlong offset)
  {
  volatile jobject *addr = (jobject *) ((char *) obj + offset);
  jobject result = *addr;
  read_barrier ();
  return result;
  }

void sun::misc::Unsafe::putOrderedObject (jobject obj, jlong offset, jobject value)
  {
  volatile jobject *addr = (jobject *) ((char *) obj + offset);
  *addr = value;
  }

  在上述Unsafe几个方法的源代码中,可以看到有write_barrierread_barrier这两个内存屏障,这两个就是对应的硬件中的写屏障和读屏障,java内存模型中使用的所谓的LoadLoad、LoadStore、StoreStore、StoreLoad这几个屏障就是基于这两个屏障实现的。写屏障的作用就是禁止了指令的重排序,并且配合C语言中的volatile关键字(C中的volatile关键字只能保证可见性不能保证有序性),个人理解就是通过添加内存屏障+C中的Volatile实现了类似Java中的Volatile关键字语义,即在putObjectVolatile方法中通过内存屏障保证了有序性,再通过volatile保证将对指定地址的操作是马上写入到共享的主存中而不是线程自身的本地工作内存中,这样配合下面的getObjectVolatile方法,就可以确保每次读取到的就是最新的数据。
  对于getObjectVolatile而言,可以看到它在返回前加了read_barrier,这个读屏障的作用就是强制去读取主存中的数据而不是线程自己的本地工作内存,这样就确保了读取到的一定是最新的数据。
  最后就是putOrderedObject,这个方法和putObjectVolatile的区别源码中在于没有加write_barrier,个人理解是这个方法只保证了更新数据的可见性,但是无法保证有序性,因为没有添加屏障可能会导致最终生成的汇编指令被重排序优化,不过在ConcurrentHashMap中使用到这个方法的地方主要是在put方法更新数据的时候用到了,而关于put是加锁了的,所以个人理解的是在依据加锁过的代码区域,用putOrderedObjectputObjectVolatile好在不需要添加屏障,因为只会有一个线程进行操作,从而允许进行指令优化重排序,从而性能会更好。
以上内容主要参考自:

Unsafe源码 https://my.oschina.net/weichou/blog/704843
C语言内存屏障和volatile关键字:https://www.cnblogs.com/god-of-death/p/7852394.html
java内存屏障相关:https://www.jianshu.com/p/c9ac99b87d56
         https://blog.csdn.net/javazejian/article/details/72772470
         https://blog.csdn.net/javazejian/article/details/72772461#可见性
         https://tech.meituan.com/java_memory_reordering.html

jdk1.7版本ConcurrentHashMap实现分析

  前面依据说过这里主要依据源码的注释进行分析,对于源码如何进行编译可以查看本人之前的博客 https://blog.csdn.net/klordy_123/article/details/82915450 。对于1.7的数据存储结构如图:image
  如上图思想主要是将数据存到一个Segment数组中,而且每个Segment数组均继承自ReentrantLock,即每个数组中是一个自带一把锁的数据结构,每当需要修改对应位置内容时,可以先对需要修改的Segment加锁,这就是1.7中ConcurrentHashMap支持高并发的分段锁技术。
  再来说每个Segment就是一个哈希数组,对应每个槽位中是一个个的链表(数据结构为HashEntry),即通过链地址法来解决插入时遇到的哈希冲突。针对以上结构的分析,我们对源码的分析先从小到大:HashEntry–> Segment–> ConcurrentHashMap

HashEntry数据结构

  HashEntry就是ConcurrentHashMap数据结构中最小的存储单元,它就是对应一个个的<k,v>节点,它的内部相对简单:

static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        /**
         * Sets next field with volatile write semantics.  (See above
         * about use of putOrderedObject.)
         */
        final void setNext(HashEntry<K,V> n) {
            UNSAFE.putOrderedObject(this, nextOffset, n);
        }

        // Unsafe mechanics
        static final sun.misc.Unsafe UNSAFE; //可以理解为一个指针
        static final long nextOffset;//偏移量,可以简单的理解为内存地址
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();//获取这个节点对应的内存指针
                Class k = HashEntry.class;//
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next")); //获取当前节点的next节点对于当前节点指针的偏移量
                    //通过UNSAFE中有方法直接能够获取到当前引用变量的初始内存地址
                    //通过初始内存地址和引用变量内部的局部变量的偏移量就可以通过Unsafe直接读取到对应的参数值
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

  源码中可以看出它的结构很简单,可以说明的一点就是其中后半部分的关于Unsafe内容的静态代码块和静态变量,由于ConcurrentHashMap中大量使用Unsafe中的方法,所以在其它用到的数据结构中也会有类似的关于Unsafe的静态变量和静态代码块,原理和这里是一样的。

Segment数据结构

  关于Segment内部的实现相对HashEntry肯定是要复杂一点的,这里份两部分介绍,首先介绍它内部的成员变量,然后再介绍它内部作为ConcurrentHashMap中相关节点操作的被代理方法。

Segment成员变量
    /**
     * scanAndLockForPut中自旋循环获取锁的最大自旋次数。
     */
    static final int MAX_SCAN_RETRIES =
        Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

    /**
     * 存储结构
     */
    transient volatile HashEntry<K,V>[] table;

    /**
     * 元素的个数,这里没有加volatile修饰,所以只能在加锁或者确保可见性(如Unsafe.getObjectVolatile)的情况下进行访问,不然无法保证数据的正确性
     */
    transient int count;

    /**
     * segment元素修改次数记录,由于未进行volatile修饰,所以访问规则和count类似
     */
    transient int modCount;

    /**
     * 扩容指标
     */
    transient int threshold;

    /**
     * @serial
     * 负载因子
     */
    final float loadFactor;

  为了不影响篇幅,已经把对应的英文注释给删除,这里面唯一有点难以直接理解的就是MAX_SCAN_RETRIES这个变量,针对这个直接在接下来的方法介绍部分先讲解和这个参数关联的方法。

Segment中的方法
scanAndLockForPut方法

  这个方法的分析查看如下源码以及其中的注释内容:

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // negative while locating node
    //如果尝试加锁失败,那么就对segment[hash]对应的链表进行遍历找到需要put的这个entry所在的链表中的位置,
    //这里之所以进行一次遍历找到坑位,主要是为了通过遍历过程将遍历过的entry全部放到CPU高速缓存中,
    //这样在获取到锁了之后,再次进行定位的时候速度会十分快,这是在线程无法获取到锁前并等待的过程中的一种预热方式。
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        //获取锁失败,初始时retries=-1必然开始先进入第一个if
        if (retries < 0) {//<1>
            if (e == null) { //<1.1>
                //e=null代表两种意思,第一种就是遍历链表到了最后,仍然没有发现指定key的entry;
                //第二种情况是刚开始时确实太过entryForHash找到的HashEntry就是空的,即通过hash找到的table中对应位置链表为空
                //当然这里之所以还需要对node==null进行判断,是因为有可能在第一次给node赋值完毕后,然后预热准备工作已经搞定,
                //然后进行循环尝试获取锁,在循环次数还未达到<2>以前,某一次在条件<3>判断时发现有其它线程对这个segment进行了修改,
                //那么retries被重置为-1,从而再一次进入到<1>条件内,此时如果再次遍历到链表最后时,因为上一次遍历时已经给node赋值过了,
                //所以这里判断node是否为空,从而避免第二次创建对象给node重复赋值。
                if (node == null) // speculatively create node
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            else if (key.equals(e.key))//<1.2>   遍历过程发现链表中找到了我们需要的key的坑位
                retries = 0;
            else//<1.3>   当前位置对应的key不是我们需要的,遍历下一个
                e = e.next;
        }
        else if (++retries > MAX_SCAN_RETRIES) {//<2>
            // 尝试获取锁次数超过设置的最大值,直接进入阻塞等待,这就是所谓的有限制的自旋获取锁,
            //之所以这样是因为如果持有锁的线程要过很久才释放锁,这期间如果一直无限制的自旋其实是对系统性能有消耗的,
            //这样无限制的自旋是不利的,所以加入最大自旋次数,超过这个次数则进入阻塞状态等待对方释放锁并获取锁。
            lock();
            break;
        }
        else if ((retries & 1) == 0 &&
                 (f = entryForHash(this, hash)) != first) {//<3>
            // 遍历过程中,有可能其它线程改变了遍历的链表,这时就需要重新进行遍历了。
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

  其中的核心思想就是通过MAX_SCAN_RETRIES控制自旋次数,防止无限制的重复自旋浪费资源。这个方法很显然见名知意,它的作用就是遍历获取锁然后进行数据插入,Segment中还有一个和这个方法十分类似的scanAndLock方法,它的实现思想和这个方法基本一致,不过这里的scanAndLockForPut主要是用在数据插入中,而scanAndLock则主要用在removereplace方法中。接下来对Segmentput方法进行分析。

Segment的put方法分析

  分析内容参考如下源码及其注释:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    //先尝试对segment加锁,如果直接加锁成功,那么node=null;如果加锁失败,则会调用scanAndLockForPut方法去获取锁,
    //在这个方法中,获取锁后会返回对应HashEntry(要么原来就有要么新建一个)
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        //这里是一个优化点,由于table自身是被volatile修饰的,然而put这一块代码本身是加锁了的,所以同一时间内只会有一个线程操作这部分内容,
        //所以不再需要对这一块内的变量做任何volatile修饰,因为变量加了volatile修饰后,变量无法进行编译优化等,会对性能有一定的影响
        //故将table赋值给put方法中的一个局部变量,从而使得能够减少volatile带来的不必要消耗。
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        //这里有一个问题:为什么不直接使用数组下标获取HashEntry,而要用entryAt来获取链表?
        //这里结合网上内容个人理解是:由于Segment继承的是ReentrantLock,所以它是一个可重入锁,那么是否存在某种场景下,
        //会导致同一个线程连续两次进入put方法,而由于put最终使用的putOrderedObject只是禁止了写写重排序无法保证内存可见性,
        //所以这种情况下第二次put在获取链表时必须用entryAt中的volatile语义的get来获取链表,因为这种情况下下标获取的不一定是最新数据。
        //不过并没有想到哪里会存在这种场景,有谁能想到的或者是我的理解有误请指出!
        HashEntry<K,V> first = entryAt(tab, index);//先获取需要put的<k,v>对在当前这个segment中对应的链表的表头结点。

        for (HashEntry<K,V> e = first;;) {//开始遍历first为头结点的链表
            if (e != null) {//<1>
                //e不为空,说明当前键值对需要存储的位置有hash冲突,直接遍历当前链表,如果链表中找到一个节点对应的key相同,
                //依据onlyIfAbsent来判断是否覆盖已有的value值。
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    //进入这个条件内说明需要put的<k,y>对应的key节点已经存在,直接判断是否更新并最后break退出循环。
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;//未进入上面的if条件中,说明当前e节点对应的key不是需要的,直接遍历下一个节点。
            }
            else {//<2>
                //进入到这个else分支,说明e为空,对应有两种情况下e可能会为空,即:
                // 1>. <1>中进行循环遍历,遍历到了链表的表尾仍然没有满足条件的节点。
                // 2>. e=first一开始就是null(可以理解为即一开始就遍历到了尾节点)
                if (node != null) //这里有可能获取到锁是通过scanAndLockForPut方法内自旋获取到的,这种情况下依据找好或者说是新建好了对应节点,node不为空
                    node.setNext(first);
                else// 当然也有可能是这里直接第一次tryLock就获取到了锁,从而node没有分配对应节点,即需要给依据插入的k,v来创建一个新节点
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1; //总数+1 在这里依据获取到了锁,即是线程安全的!对应了上述对count变量的使用规范说明。
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)//判断是否需要进行扩容
                    //扩容是直接重新new一个新的HashEntry数组,这个数组的容量是老数组的两倍,
                    //新数组创建好后再依次将老的table中的HashEntry插入新数组中,所以这个过程是十分费时的,应尽量避免。
                    //扩容完毕后,还会将这个node插入到新的数组中。
                    rehash(node);
                else
                    //数组无需扩容,那么就直接插入node到指定index位置,这个方法里用的是UNSAFE.putOrderedObject
                    //网上查阅到的资料关于使用这个方法的原因都是说因为它使用的是StoreStore屏障,而不是十分耗时的StoreLoad屏障
                    //给我个人感觉就是putObjectVolatile是对写入对象的写入赋予了volatile语义,但是代价是用了StoreLoad屏障
                    //而putOrderedObject则是使用了StoreStore屏障保证了写入顺序的禁止重排序,但是未实现volatile语义导致更新后的不可见性,
                    //当然这里由于是加锁了,所以在释放锁前会将所有变化从线程自身的工作内存更新到主存中。
                    //这一块对于putOrderedObject和putObjectVolatile的区别有点混乱,不是完全理解,网上也没找到详细解答,查看了C源码也是不大确定。
                    //希望有理解的人看到能指点一下,后续如果弄明白了再更新这一块。
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

  如上为put方法的分析,已经解释的十分详细,接下来对其中用到的rehash扩容方法进行分析。

Segment的rehash扩容分析
/**
 * Doubles size of table and repacks entries, also adding the
 * given node to new table
 * 对数组进行扩容,由于扩容过程需要将老的链表中的节点适用到新数组中,所以为了优化效率,可以对已有链表进行遍历,
 * 对于老的oldTable中的每个HashEntry,从头结点开始遍历,找到第一个后续所有节点在新table中index保持不变的节点fv,
 * 假设这个节点新的index为newIndex,那么直接newTable[newIndex]=fv,即可以直接将这个节点以及它后续的链表中内容全部直接复用copy到newTable中
 * 这样最好的情况是所有oldTable中对应头结点后跟随的节点在newTable中的新的index均和头结点一致,那么就不需要创建新节点,直接复用即可。
 * 最坏情况当然就是所有节点的新的index全部发生了变化,那么就全部需要重新依据k,v创建新对象插入到newTable中。
*/
@SuppressWarnings("unchecked")
private void rehash(HashEntry<K,V> node) {
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    int newCapacity = oldCapacity << 1;
    threshold = (int)(newCapacity * loadFactor);
    HashEntry<K,V>[] newTable =
        (HashEntry<K,V>[]) new HashEntry[newCapacity];
    int sizeMask = newCapacity - 1;
    for (int i = 0; i < oldCapacity ; i++) {
        HashEntry<K,V> e = oldTable[i];
        if (e != null) {
            HashEntry<K,V> next = e.next;
            int idx = e.hash & sizeMask;
            if (next == null)   //  Single node on list 只有单个节点
                newTable[idx] = e;
            else { // Reuse consecutive sequence at same slot
                HashEntry<K,V> lastRun = e;
                int lastIdx = idx;
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }//这个for循环就是找到第一个后续节点新的index不变的节点。
                newTable[lastIdx] = lastRun;
                // Clone remaining nodes
                //第一个后续节点新index不变节点前所有节点均需要重新创建分配。
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, p.value, n);
                }
            }
        }
    }
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}

  关于Segment类中方法主要介绍的就是上面几个方法,理解了以上几个方法的思路后,至于其它还有remove、replace等方法实现逻辑有部分类似的地方,再理解起来就较为简单,不再赘述。

ConcurrentHashMap的成员变量和方法

   分析完HashEntrySegment的结构和方法并了解了Unsafe类中某些方法后,再来看ConcurrentHashMap中的方法就相对简单多了。不过这里关于ConcurrentHashMap有一点需要知道的是,它内部的Segment数组是延迟创建的,就是说刚开始初始化的时候,只有segments[0]这个Segment是被创建的,其它所有的Segment均是不会创建的,只有在访问到或者用到这个Segment的时候才会创建,针对这个相关的一个核心方法就是ensureSegment方法。不过在介绍这些方法前,先介绍ConcurrentHashMap的成员变量。

ConcurrentHashMap的成员变量
/**
 * 在构造函数未指定初始大小时,默认使用的map大小
 */
static final int DEFAULT_INITIAL_CAPACITY = 16;

/**
 * 默认的扩容因子,当初始化构造器中未指定时使用。
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 默认的并发度,这里所谓的并发度就是能同时操作ConcurrentHashMap(后文简称为chmap)的线程的最大数量,
 * 由于chmap采用的存储是分段存储,即多个segement,加锁的单位为segment,所以一个cmap的并行度就是segments数组的长度,
 * 故在构造函数里指定并发度时同时会影响到cmap的segments数组的长度,因为数组长度必须是大于并行度的最小的2的幂。
 */
static final int DEFAULT_CONCURRENCY_LEVEL = 16;

/**
 * 最大容量
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 每个分段最小容量
 */
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

/**
 * 分段最大的容量
 */
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative

/**
 * 默认自旋次数,超过这个次数直接加锁,防止在size方法中由于不停有线程在更新map
 * 导致无限的进行自旋影响性能,当然这种会导致ConcurrentHashMap使用了这一规则的方法
 * 如size、clear是弱一致性的。
 */
static final int RETRIES_BEFORE_LOCK = 2;

/**
 * 用于索引segment的掩码值,key哈希码的高位用于选择segment
 */
final int segmentMask;

/**
 * 用于索引segment偏移值
 */
final int segmentShift;

/**
 * Segment数组
 */
final Segment<K,V>[] segments;

transient Set<K> keySet;
transient Set<Map.Entry<K,V>> entrySet;
transient Collection<V> values;
构造函数分析
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;
    //依据给定的concurrencyLevel并行度,找到最适合的segments数组的长度,
    // 为上文默认并行度参数说明的大于concurrencyLevel的最小的2的n次方
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
    // create segments and segments[0]
    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;
}

  这里关于concurrencyLevel这个并发度参数的对ConcurrentHashMap的性能影响十分重要,如果设置过小就会导致严重的锁竞争问题,而设置过大会使得原本可以在同一个Segment内访问的数据扩散到不同```Segment``而降低了CPU cache的命中率,那么究竟该如何合理的设置这个值呢?这个问题暂未找到答案,先标记一下。

ConcurrentHashMap方法分析

  针对以上介绍,这里先说明一下延迟创建segments数组中用到的ensureSegment方法。

ensureSegment方法
private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        Segment<K,V> proto = ss[0]; // 以初始化时创建的第一个坑位的ss[0]作为模版进行创建
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // 二次检查是否有其它线程创建了这个Segment
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                //这里通过自旋的CAS方式对segments数组中偏移量为u位置设置值为s,这是一种不加锁的方式,
                //万一有多个线程同时执行这一步,那么只会有一个成功,而其它线程在看到第一个执行成功的线程结果后
                //会获取到最新的数据从而发现需要更新的坑位已经不为空了,那么就跳出while循环并返回最新的seg
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

  这个方法核心思想就是利用自旋CAS来创建对应Segment,这种思想是之后不加锁保证线程安全的一个十分典型的实现方式。除了这个方法关于segments数组还有一些其它实现较为简单的例如:segmentAtsegmentForHash等方法,这些方法就是利用Unsafe中的方法去实现从主存中获取最新数据或是直接往主存中写入最新数据,实现代码逻辑十分简单,对于这些方法不再赘述。

get方法
public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    int h = hash(key);//获取key对应hash值
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;//获取对应h值存储所在segments数组中内存偏移量
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        //通过Unsafe中的getObjectVolatile方法进行volatile语义的读,获取到segments在偏移量为u位置的分段Segment,
        //并且分段Segment中对应table数组不为空
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {//获取h对应这个分段中偏移量为xxx下的HashEntry的链表头结点,然后对链表进行 遍历
            //###这里第一次初始化通过getObjectVolatile获取HashEntry时,获取到的是主存中最新的数据,但是在后续遍历过程中,有可能数据被其它线程修改
            //从而导致其实这里最终返回的可能是过时的数据,所以这里就是ConcurrentHashMap所谓的弱一致性的体现,containsKey方法也一样!!!!
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

  这里需要注意的就是这个方法是弱一致性的,所以有可能会获取到过时的数据,如果业务场景要求获取数据的强一致性,不建议用这个。
  另外对于其它的例如put、repace、remove等方法,均是直接调用Segment中的方法,逻辑在Segment中已经有了介绍,这里针对这些方法不再进行介绍,最后再介绍的一个方法就是size方法,主要是这个方法中有些许不同。

size方法
public int size() {
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // 是否溢出
    long sum;         // 存储本次循环过程中计算得到的modCount的值
    long last = 0L;   // 存储上一次遍历过程中计算得到的modCount的和
    int retries = -1; // first iteration isn't retry
    try {
        for (;;) {//无限for循环,结束条件就是任意前后两次遍历过程中modcount值的和是一样的,说明第二次遍历没有做任何变化
            //这里就是前面介绍的为了防止由于有线程不断在更新map而导致每次遍历过程一直发现modCount和上一次不一样
            //从而导致线程一直进行遍历验证前后两次modCount,为了防止这种情况发生,加了一个最多重复的次数限制,
            //超过这个次数则直接强制对所有的segment进行加锁,不过这里需要注意如果出现这种情况,会导致本来要延迟创建的所有segment
            //均在这个过程中被创建
            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 {
        //由于只有在retries等于RETRIES_BEFORE_LOCK时才会执行强制加锁,并且由于是用的retries++,
        //所以强制加锁完毕后,retries的值是一定会大于RETRIES_BEFORE_LOCK的,
        //这样就防止正常遍历而没进行加锁时进行锁释放的情况
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}

  只要理解了size方法中的思路,其它几个需要遍历整个map的方法如:containsValue、isEmpty思想和这个是一样的,理解其它就十分easy了,这里不再赘述。花了两天时间才把这里搞定,之后还有1.8的源码有6000+行,想想就有点担忧,这个十一的计划怕是要完不成了。。。
参考博客:https://my.oschina.net/7001/blog/896587

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值