• 源码分析 CurrentHashMap 1.7


    1.0 数据结构

      

    ConcurrentHashMap 是由 Segment 数组 结构和 HashEntry 数组 结构组成。

    • Segment 是一种可重入锁 ReentrantLock,在 ConcurrentHashMap 里扮演锁的角色,HashEntry 则用于存储键值对数据。
    • ConcurrentHashMap 里包含一个 Segment 数组,Segment 的结构和 HashMap 类似,一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护者一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。

    2.0 构造函数

    属性说明
    concurrencyLevel 并发度,程序运行时能够同时更新 ConcurrentHashMap 且不产生锁竞争的最大线程数,分段锁个数,即 Segment[] 的数组长度,默认为 16。用户也可以在构造函数中设置并发度。
    initialCapacity 初始容量,指的是整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。
    loadFactor 负载因子,Segment 数组不可以扩容,负载因子供每个 Segment 内部使用。
    • 和 JDK 1. 6 不同,JDK 1. 7 中除了第一个 Segment 之外,剩余的 Segments 采用的是 延迟初始化 机制:每次 put 之前都需要检查 key 对应的 Segment 是否为 null,如果是则调用 ensureSegment() 以确保对应的 Segment 被创建。
    • ensureSegment() 可能在并发环境下被调用,但并未使用锁来控制竞争,而是使用了 Unsafe 对象的 getObjectVolatile() 提供的原子读语义结合 CAS 来确保 Segment 创建的原子性

       

    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;
        // 计算并行级别 ssize,因为要保持并行级别是 2 的 n 次方
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        // 默认值,concurrencyLevel 为 16,sshift 为 4
        // 那么计算出 segmentShift 为 28,segmentMask 为 15,后面会用到这两个值
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
     
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
     
        // initialCapacity 是设置整个 map 初始的大小,
        // 这里根据 initialCapacity 计算 Segment 数组中每个位置可以分到的大小
        // 如 initialCapacity 为 64,那么每个 Segment 或称之为"槽"可以分到 4 个
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        // 默认 MIN_SEGMENT_TABLE_CAPACITY 是 2,这个值也是有讲究的,因为这样的话,对于具体的槽上,
        // 插入一个元素不至于扩容,插入第二个的时候才会扩容
        int cap = MIN_SEGMENT_TABLE_CAPACITY; 
        while (cap < c)
            cap <<= 1;
     
        // 创建 Segment 数组,
        // 并创建数组的第一个元素 segment[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];
        // 往数组写入 segment[0]
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }
    • Segment 数组长度为 16,不可以扩容。
    • Segment[i] 的默认大小为 2,负载因子是 0.75,得出初始阈值为 1.5,也就是以后插入第一个元素不会触发扩容,插入第二个会进行第一次扩容。
    • 只初始化了 segment[0],其他位置仍然是 null。
    • 当前 segmentShift 的值为 32 - 4 = 28,segmentMask 为 16 - 1 = 15,为移位数和掩码。

    3.0 put方法

    public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        // 1. 计算 key 的 hash 值
        int hash = hash(key);
        // 2. 根据 hash 值找到 Segment 数组中的位置 j
        //    hash 是 32 位,无符号右移 segmentShift(28) 位,剩下低 4 位,
        //    然后和 segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的最后 4 位,也就是槽的数组下标
        int j = (hash >>> segmentShift) & segmentMask;
        // 初始化的时候只初始化了 segment[0],其他位置还是 null,
        // ensureSegment(j) 对 segment[j] 进行初始化
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);  //初始化槽
        // 3. 插入新值到 槽 s 中
        return s.put(key, hash, value, false);  //开始插入
    }
    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        // 在往该 segment 写入前,需要先获取该 segment 的独占锁,获取失败尝试获取自旋锁
        HashEntry<K,V> node = tryLock() ? null :
            scanAndLockForPut(key, hash, value);
        V oldValue;
        try {
            // segment 内部的数组
            HashEntry<K,V>[] tab = table;
            // 利用 hash 值,求应该放置的数组下标
            int index = (tab.length - 1) & hash;
            // first 是数组该位置处的链表的表头
            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 {
                    // node 是不是 null,这个要看获取锁的过程。
                    // 如果不为 null,那就直接将它设置为链表表头;如果是 null,初始化并设置为链表表头。
                    if (node != null)
                        node.setNext(first);
                    else
                        node = new HashEntry<K,V>(hash, key, value, first);
     
                    int c = count + 1;
                    // 如果超过了该 segment 的阈值,这个 segment 需要扩容
                    if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                        rehash(node); // 扩容
                    else
                        // 没有达到阈值,将 node 放到数组 tab 的 index 位置,
                        // 将新的结点设置成原链表的表头
                        setEntryAt(tab, index, node);
                    ++modCount;
                    count = c;
                    oldValue = null;
                    break;
                }
            }
        } finally {
            // 解锁
            unlock();
        }
        return oldValue;
    }
    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[0] 处的数组长度和负载因子来初始化 segment[k],这就是之前要初始化 segment[0] 的原因。
            // 为什么要用 " 当前 ",因为 segment[0] 可能早就扩容过了。
            Segment<K,V> proto = ss[0];
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
     
            // 初始化 segment[k] 内部的数组
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // 再次检查一遍该槽是否被其他线程初始化。
     
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                // 使用 while 循环,内部用 CAS,当前线程成功设值或其他线程成功设值后,退出
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }
                                                                            
    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
     
        // 循环获取锁
        while (!tryLock()) {
            HashEntry<K,V> f; // to recheck first below
            if (retries < 0) {
                if (e == null) {
                    if (node == null) // speculatively create node
                        // 进到这里说明数组该位置的链表是空的,没有任何元素
                        // 当然,进到这里的另一个原因是 tryLock() 失败,所以该槽存在并发,不一定是该位置
                        node = new HashEntry<K,V>(hash, key, value, null);
                    retries = 0;
                }
                else if (key.equals(e.key))
                    retries = 0;
                else
                    // 顺着链表往下走
                    e = e.next;
            }
            // 重试次数如果超过 MAX_SCAN_RETRIES(单核 1 次多核 64 次),那么不抢了,进入到阻塞队列等待锁
            //    lock() 是阻塞方法,直到获取锁后返回
            else if (++retries > MAX_SCAN_RETRIES) {
                lock();
                break;
            }
            else if ((retries & 1) == 0 &&
                     // 进入这里,说明有新的元素进到了链表,并且成为了新的表头
                     // 这边的策略是,重新执行 scanAndLockForPut 方法
                     (f = entryForHash(this, hash)) != first) {
                e = first = f; // re-traverse if entry changed
                retries = -1;
            }
        }
        return node;
    }

      segment 数组不能扩容,是对 segment 数组某个位置内部的数组 HashEntry[] 进行扩容,扩容后容量为原来的 2 倍,该方法没有考虑并发,因为执行该方法之前已经获取了锁

    // 方法参数上的 node 是这次扩容后,需要添加到新的数组中的数据。
    private void rehash(HashEntry<K,V> node) {
        HashEntry<K,V>[] oldTable = table;
        int oldCapacity = oldTable.length;
        // 2 倍
        int newCapacity = oldCapacity << 1;
        threshold = (int)(newCapacity * loadFactor);
        // 创建新数组
        HashEntry<K,V>[] newTable =
            (HashEntry<K,V>[]) new HashEntry[newCapacity];
        // 新的掩码,如从 16 扩容到 32,那么 sizeMask 为 31,对应二进制 ‘000...00011111’
        int sizeMask = newCapacity - 1;
     
        // 遍历原数组,将原数组位置 i 处的链表拆分到 新数组位置 i 和 i+oldCap 两个位置
        for (int i = 0; i < oldCapacity ; i++) {
            // e 是链表的第一个元素
            HashEntry<K,V> e = oldTable[i];
            if (e != null) {
                HashEntry<K,V> next = e.next;
                // 计算应该放置在新数组中的位置,
                // 假设原数组长度为 16,e 在 oldTable[3] 处,那么 idx 只可能是 3 或者是 3 + 16 = 19
                int idx = e.hash & sizeMask;
                if (next == null)   // 该位置处只有一个元素
                    newTable[idx] = e;
                else { // Reuse consecutive sequence at same slot
                    // e 是链表表头
                    HashEntry<K,V> lastRun = e;
                    // idx 是当前链表的头结点 e 的新位置
                    int lastIdx = idx;
     
                    // for 循环找到一个 lastRun 结点,这个结点之后的所有元素是将要放到一起的
                    for (HashEntry<K,V> last = next;
                         last != null;
                         last = last.next) {
                        int k = last.hash & sizeMask;
                        if (k != lastIdx) {
                            lastIdx = k;
                            lastRun = last;
                        }
                    }
                    // 将 lastRun 及其之后的所有结点组成的这个链表放到 lastIdx 这个位置
                    newTable[lastIdx] = lastRun;
                    // 下面的操作是处理 lastRun 之前的结点,
                    //    这些结点可能分配在另一个链表中,也可能分配到上面的那个链表中
                    for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                        V v = p.value;
                        int h = p.hash;
                        int k = h & sizeMask;
                        HashEntry<K,V> n = newTable[k];
                        newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                    }
                }
            }
        }
        // 将新来的 node 放到新数组中刚刚的 两个链表之一 的 头部
        int nodeIndex = node.hash & sizeMask; // add the new node
        node.setNext(newTable[nodeIndex]);
        newTable[nodeIndex] = node;
        table = newTable;
    }

      总结:

      put 方法的流程。

    1. 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
    2. 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
    3. 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
    4. 最后再解除在第 1 步中所获取当前 Segment 的锁。

    4.0 get方法流程

      

    public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        // 1. hash 值
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        // 2. 根据 hash 找到对应的 segment
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
            // 3. 找到segment 内部数组相应位置的链表,遍历
            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;
    }

      总结:

    • get 方法的流程。
    1. 计算 hash 值,找到 segment 数组中的具体位置,获使用的槽。
    2. 槽中也是一个数组,根据 hash 找到数组中具体的位置。
    3. 顺着链表进行查找即可。
    • 因为 get 过程中没有加锁,因此需要考虑并发问题

    5.0 其它

    size

    • 要统计整个 ConcurrentHashMap 里元素的大小,就必须统计所有 Segment 里元素的大小后求和。

      • Segment 里的全局变量 count 是一个 volatile 变量。
    • ConcurrentHashMap 的做法是先尝试 2 次通过不锁住 Segment 的方式统计各个 Segment 大小,如果统计的过程中,容器的 count 发生了变化,则再采用加锁的方式来统计所有 Segment 的大小

      • 使用 modCount 变量,在 put、remove 和 clean 方法里操作元素前都会将变量 modCount 进行加 1,在统计 size 前后比较 modCount 是否发生变化,从而得知容器的大小是否发生变化。
  • 相关阅读:
    游戏性能保障体系
    一个ServiceHost寄宿多个服务
    EntLib PIAB 自定义CallHandler的一个BUG
    从MDK分散加载文件学习STM32启动流程
    .net 2.04.6下载
    QQ输入法使用「智能英文」模式(CTRL+SHIFT+E),快速输入英文单词
    QQ拼音输入法自定义短语(①②③≥≈÷★)
    指法输入中文打字俱乐部(TypingClub)是一款可以让用户从 0 开始练习打字的在线服务
    文件批量改名工具(RefilesName V2.0.exe)需要用安装「32位的VC++ 2005」
    博客园的「网摘」crx浏览器插件不错,支持键入多个标签
  • 原文地址:https://www.cnblogs.com/helloworldmybokeyuan/p/11714013.html
Copyright © 2020-2023  润新知