ConcurrentHashMap源码解析

底层数据结构:

  • 底层实现和HashMap是一致的,各种特性也是基本保持一致。

与1.7版本的区别:

  • 取消了segment的分段设置,直接使用Node数组来保存数据,并且采用Node数组元素作为锁来实现每一行数据加锁进一步减少并发冲突的概率。
  • 将数组和单项链表的机构变成了数组+单向链表+红黑树结构。

成员变量:

  /**********************************************HashMap中也有的*******************************************************************/
    /**
     * 最大的容量
     */
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认的初始化容量
     */
    private static final int DEFAULT_CAPACITY = 16;

    /**
     * 扩容因子
     */
    private static final float LOAD_FACTOR = 0.75f;

    /**
     * 当链表的长度大于8会转化成红黑树的点1
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 当红黑树的节点个数小于6的时候转化成链表
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 数组的最大值
     */
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 只有当容器中的元素的数量达到64的时候才可以转化成红黑树
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
     * 默认的初始化数组 用来保存元素
     */
    transient volatile Node<K, V>[] table;
    /**********************************************HashMap中也有的*******************************************************************/


    /**********************************************HashMap中没有的*******************************************************************/
    /**
     * 用来控制表初始化和扩容的
     * <p>
     * 1,默认值为0,
     * 2,当在初始化的时候指定了大小,这会将这个大小保存在sizeCtl中,大小为数组的0.75
     * 3,当为负的时候,说明表正在初始化或扩张,
     * <p>
     * -1:表示初始化
     * -(1+n) n:表示活动的扩张线程
     * 0:默认值,后续在真正初始化的时候使用默认容量
     * > 0:初始化或扩容完成后下一次的扩容门槛
     */
    private transient volatile int sizeCtl;

    /**
     * 转移的时候用的数组
     */
    private transient volatile Node<K, V>[] nextTable;

    /**
     *  默认并发数 Segment 分段锁 默认的段 未被使用  此表的默认并发级别。未使用但是  定义为与此类的先前版本兼容。
     */
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    /**
     * 用于记录元素的个数
     */
    private transient volatile long baseCount;
    
    /**
     * 计数器表。当非null时,size是2的幂。
     * counterCells是一个元素为CounterCell的数组,该数组的大小与当前机器的CPU数量有关,并且它不会被主动初始化,只有在调用fullAddCount()函数时才会进行初始化。
     */
    private transient volatile CounterCell[] counterCells;

    // views
    private transient KeySetView<K, V> keySet;
    private transient ValuesView<K, V> values;
    private transient EntrySetView<K, V> entrySet;
    
    // 扩容时使用的的索引
    private transient volatile int transferIndex;

    // 旋转锁
    private transient volatile int cellsBusy;

    //每次进行转移的最小值这个值作为一个下限来避免Rsisize遇到过多的内存争用
    private static final int MIN_TRANSFER_STRIDE = 16;

    // 生成sizeCtl所使用的bit位数
    private static final int RESIZE_STAMP_BITS = 16;

    // 进行扩容所允许的最大线程数
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

    // 记录sizeCtl中的大小所需要进行的偏移位数
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

    // 一系列的标识
    static final int MOVED = -1; //  表示正在转移
    static final int TREEBIN = -2; //  表示已经转移成树
    static final int RESERVED = -3;
    static final int HASH_BITS = 0x7fffffff; // 正常节点哈希的可用位 1111111111111111111111111111111

    // 获取可用的CPU个数
    static final int NCPU = Runtime.getRuntime().availableProcessors();

    // 进行序列化的属性
    private static final ObjectStreamField[] serialPersistentFields = {
            new ObjectStreamField("segments", Segment[].class),
            new ObjectStreamField("segmentMask", Integer.TYPE),
            new ObjectStreamField("segmentShift", Integer.TYPE),
    };
    /**********************************************Unsafe*******************************************************************/
    // 在ConcurrentHashMap中使用了unSafe方法,通过直接操作内存的方式来保证并发处理的安全性,使用的是硬件的安全机制
    private static final Unsafe U = Unsafe.getUnsafe();
    private static final long SIZECTL;
    private static final long TRANSFERINDEX;
    private static final long BASECOUNT;
    private static final long CELLSBUSY;
    private static final long CELLVALUE;
    private static final int ABASE;
    private static final int ASHIFT;

    static {
        SIZECTL = U.objectFieldOffset
                (ConcurrentHashMap.class, "sizeCtl");
        TRANSFERINDEX = U.objectFieldOffset
                (ConcurrentHashMap.class, "transferIndex");
        BASECOUNT = U.objectFieldOffset
                (ConcurrentHashMap.class, "baseCount");
        CELLSBUSY = U.objectFieldOffset
                (ConcurrentHashMap.class, "cellsBusy");

        CELLVALUE = U.objectFieldOffset
                (CounterCell.class, "value");

        ABASE = U.arrayBaseOffset(Node[].class);
        int scale = U.arrayIndexScale(Node[].class);
        if ((scale & (scale - 1)) != 0)
            throw new ExceptionInInitializerError("array index scale not a power of two");
        ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);

        Class<?> ensureLoaded = LockSupport.class;

        ensureLoaded = ReservationNode.class;
    }
    /**********************************************Unsafe*******************************************************************/
    /**********************************************HashMap中没有的*******************************************************************/
  • sizeCtl(重点):
  •  用来控制表初始化和扩容的
     -1:表示初始化
     -(1+n) n:表示活动的扩张线程
     0:默认值,后续在真正初始化的时候使用默认容量
     > 0:初始化或扩容完成后下一次的扩容门槛
    

三个原子操作的方法

 /*******************************【 三个原子的方法 】**************************/

    /**
     * 返回数组指定位置的的节点原子操作
     */
    @SuppressWarnings("unchecked")
    static final <K, V> Node<K, V> tabAt(Node<K, V>[] tab, int i) {
        return (Node<K, V>) U.getObjectAcquire(tab, ((long) i << ASHIFT) + ABASE);
    }

    /**
     *  在指定位置设置值  
     *  tab 要修改的数组
     *  i   要设置的节点的索引值
     *  c   期望值
     *  v   修改的值
     *  expcession:
     *       利用CAS算法设置i位置上的Node节点。
     *       当且仅当tab[i]=c的时候 才修改值为v
     *
     */
    static final <K, V> boolean casTabAt(Node<K, V>[] tab, int i, Node<K, V> c, Node<K, V> v) {
        return U.compareAndSetObject(tab, ((long) i << ASHIFT) + ABASE, c, v);
    }

    /**
     * 在指定位置设置值
     * 利用volatile方法设置节点位置的值
     */
    static final <K, V> void setTabAt(Node<K, V>[] tab, int i, Node<K, V> v) {
        U.putObjectRelease(tab, ((long) i << ASHIFT) + ABASE, v);
    }

    /*******************************【 三个原子的方法 】**************************/
  • 在集合中出现频率高的一匹

构造方法

 /**************************************  【构造方法】  *************************************************/
    /**
     * 默认的构造方法
     */
    public ConcurrentHashMap() {
    }

    /**
     * 这里多了一个参数
     * 传入初始化参数
     */
    public ConcurrentHashMap(int initialCapacity) {
        this(initialCapacity, LOAD_FACTOR, 1);  /**传入了扩容因子*/
    }

    /**
     * @param initialCapacity  初始化容量参数
     * @param loadFactor       默认的扩容因子
     * @param concurrencyLevel 初始化的时候传入1
     */
    public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)// 默认的负载因子小于等于0则抛出异常
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)
            initialCapacity = concurrencyLevel;

        long size = (long) (1.0 + (long) initialCapacity / loadFactor);
        int cap = (size >= (long) MAXIMUM_CAPACITY) ?   // size 大于最大容量?
                MAXIMUM_CAPACITY :                      // 设置为最大容量
                tableSizeFor((int) size);               // 返回传入容量的最接近2的n次方的最大值(带扩容的  比如说传入13 得到的值是32 )

        this.sizeCtl = cap;         // 设置sizeCtl的值 也就是扩容门槛(设置的就是最大的扩容门槛也就是当前最接近传入的容量的向上取2次方)
    }
    /**************************************  【构造方法】 *************************************************/
  • sizeCtl 的值在这里设置的是当前传入的容量向上取直接近的2的次幂

Put方法解析

   /**
     * 放置元素到集合中  调用的是下面的这个put方法
     */
    public V put(K key, V value) {
        return putVal(key, value, false);
    }
   /**
     * 真正实现放置元素的方法
     */
    final V putVal(K key, V value, boolean onlyIfAbsent) {

        if (key == null || value == null) throw new NullPointerException();    // 键或值为空,抛出异常

        int hash = spread(key.hashCode());    // 计算hash值  得出来的结果和hashMap的是一样的   低位和高位相异或

        int binCount = 0;         // 要插入的元素所在桶的元素个数

        for (Node<K, V>[] tab = table; ; ) {  //死循环  几乎覆盖整合方法的大循环  结合CAS使用(如果CAS失败,则会重新取整个桶进行下面的流程)

            Node<K, V> f;int n, i, fh; K fk;V fv;

            if (tab == null || (n = tab.length) == 0) //tab未经历过初始化
                tab = initTable();  // fixme 初始化  《1》

            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //如果待插入的元素所在的桶为空,则尝试把此元素直接插入到桶的第一个位置
                if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value))) // 当tab[i]=null 的之后设置值为【new Node<K, V>(hash, key, value)】
                    break;  //插入成功跳出循环

            }

            else if ((fh = f.hash) == MOVED) // 当前元素的所在的桶的第一个元素的值为MOVED 则当前线程帮忙一起迁移元素 MOVED表示正在扩容过程中
                tab = helpTransfer(tab, f);//fixme 帮忙迁移元素(正在扩容的过程中)《2》

            else if (onlyIfAbsent && fh == hash && ((fk = f.key) == key || (fk != null && key.equals(fk))) && (fv = f.val) != null) return fv;  // 这个暂时没看懂


            else { // 不是初始化 而且当前hash值得链表的地方有值 而且当前桶不是扩容状态
                V oldVal = null;

                synchronized (f) {  // 给链表中的第一个元素加锁
                    if (tabAt(tab, i) == f) {   // 再次检测第一个元素是否有变化,如果有变化则进入下一次循环,从头来过

                        // 链表
                        if (fh >= 0) {

                            binCount = 1;  // 如果是链表那么 做为循环次数初始化值赋值为1

                            for (Node<K, V> e = f; ; ++binCount) { // 遍历整个桶,每次结束binCount加1

                                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);
                                    break;
                                }
                            }
                        }
                        // 红黑树
                        else if (f instanceof TreeBin) {  // 如果第一个元素是树节点

                            Node<K, V> p;

                            binCount = 2;  // 如果是树直接赋值为2

                            if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key, value)) != null) { //调用红黑树的插入方法插入元素,如果成功插入则返回null,否则返回寻找到的节点

                                oldVal = p.val;// 如果找到了这个元素,则赋值了新值
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        } else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }

                if (binCount != 0) { // 如果binCount不为0,说明成功插入了元素或者寻找到了元素

                    if (binCount >= TREEIFY_THRESHOLD)// 如果链表元素个数达到了8
                        treeifyBin(tab, i);//尝试树化

                    if (oldVal != null)
                        return oldVal;  // 如果要插入的元素已经存在,则返回旧值

                    break; // 退出外层大循环,流程结束
                }
            }
        }

        addCount(1L, binCount); // fixme 成功插入元素,元素个数加1(是否要扩容在这个里面) 《3》

        return null;    // 成功插入元素返回null
    }
  • 总结
  • (1)如果桶数组未初始化,则初始化;
  • (2)如果待插入的元素所在的桶为空,则尝试把此元素直接插入到桶的第一个位置;
  • (3)如果正在扩容,则当前线程一起加入到扩容的过程中;
  • (4)如果待插入的元素所在的桶不为空且不在迁移元素,则锁住这个桶(分段锁);
  • (5)如果当前桶中元素以链表方式存储,则在链表中寻找该元素或者插入元素;
  • (6)如果当前桶中元素以红黑树方式存储,则在红黑树中寻找该元素或者插入元素;
  • (7)如果元素存在,则返回旧值;
  • (8)如果元素不存在,整个Map的元素个数加1,并检查是否需要扩容;
  • 需要关注里面的三个方法
  •  (1) initTable(); 初始化方法
  •  (2) helpTransfer(tab, f); 帮助迁移元素
  •  (3) addCount(1L, binCount);插入元素并且检查是否需要扩容

 initTable(); 初始化方法

 private final Node<K, V>[] initTable() {
        Node<K, V>[] tab;
        int sc;
        while ((tab = table) == null || tab.length == 0) {

            if ((sc = sizeCtl) < 0) // 如果sizeCtl<0说明正在初始化或者扩容,让出CPU
                Thread.yield();

            /**
             * 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
             * @param obj 需要更新的对象
             * @param offset obj中要更新的变量
             * @param expect 期望值
             * @param update 如果期望值expect与offset的当前值相同,设置offset的值为这个新值
             * @return 如果field的值被更改返回true
             * public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
             */
            else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {          // 使用CAS锁控制只有一个线程初始化桶数组;
                // 如果把sizeCtl原子更新为-1成功,则当前线程进入初始化
                // 如果原子更新失败则说明有其它线程先一步进入初始化了,则进入下一次循环
                // 如果下一次循环时还没初始化完毕,则sizeCtl<0进入上面if的逻辑让出CPU
                // 如果下一次循环更新完毕了,则table.length!=0,退出循环
                try {

                    if ((tab = table) == null || tab.length == 0) {  // 再次检查table是否为空,防止ABA问题
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;  // 如果sc为0则使用默认值16
                        @SuppressWarnings("unchecked")
                        Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n];  // 新建数组
                        table = tab = nt;  // 赋值给table桶数组
                        sc = n - (n >>> 2);   //  设置sc为数组长度的0.75倍 【 n - (n >>> 2) = n - n/4 = 0.75n 】  可见这里装载因子和扩容门槛都是写死了的 这也正是没有threshold和loadFactor属性的原因
                    }
                } finally {
                    sizeCtl = sc;//把sc赋值给sizeCtl,这时存储的是扩容门槛,就是桶数组大小的0.75倍
                }
                break;
            }
        }
        return tab;
    }
  •   总结:
  • (1)使用CAS锁控制只有一个线程初始化桶数组;
  • (2)sizeCtl在初始化后存储的是扩容门槛;
  • (3)扩容门槛写死的是桶数组大小的0.75倍,桶数组大小即map的容量,也就是最多存储多少个元素。
  •    关于CAS的介绍 compareAndSet :
  •    存在三个值 当前值 预期值 新值 当且仅当当前值等于预期值才会把当前值更新为新值
  •    什么是ABA问题?
  •    假设当前值为A  一个线程把当前值A 改为B 又把B改为A 另一个线程用CAS来操作当前值还是成功的  

helpTransfer(tab, f); 帮助迁移元素

 final Node<K, V>[] helpTransfer(Node<K, V>[] tab, Node<K, V> f) {
        Node<K, V>[] nextTab;
        int sc;
        // 如果桶数组不为空,并且当前桶第一个元素为ForwardingNode类型,并且nextTab不为空,说明当前桶已经迁移完毕了,才去帮忙迁移其它桶的元素, 扩
        // 容时会把旧桶的第一个元素置为ForwardingNode,并让其nextTab指向新桶数组
        if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode<K, V>) f).nextTable) != null) {

            // 根据 length 得到一个标识符号
            int rs = resizeStamp(tab.length);

            //fixme sizeCtl<0,说明正在扩容  // 如果 nextTab 没有被并发修改 且 tab 也没有被并发修改 且 sizeCtl  < 0 (说明还在扩容)
            while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) {

                // 如果 sizeCtl 无符号右移  16 不等于 rs ( sc前 16 位如果不等于标识符,则标识符变化了)
                // 或者 sizeCtl == rs + 1  (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
                // 或者 sizeCtl == rs + 65535  (如果达到最大帮助线程的数量,即 65535)
                // 或者转移下标正在调整 (扩容结束)
                // 结束循环,返回 table
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;


                if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) { // 如果以上都不是, 将 sizeCtl + 1, (表示增加了一个线程帮助其扩容)

                    // 当前线程帮忙迁移元素
                    transfer(tab, nextTab);

                    break;
                }
            }
            return nextTab;
        }
        return table;
    }
  • 总结:
    • (1) 如果当前链表已经迁移完毕,帮其他线程迁移元素
    • (2)  迁移完成跳出循环,返回迁移完毕的新数组
    • (3)  当前链表未迁移完毕,直接返回原数组
  • 需要注意的方法
    • transfer(tab, nextTab);真正扩容的方法 扩容时容量变为两倍,并把部分元素迁移到其它桶中。

addCount(1L, binCount);插入元素并且检查是否需要扩容

private final void addCount(long x, int check) {

        CounterCell[] cs;
        long b, s;

        // 把数组的大小存储根据不同的线程存储不同的当前集合元素的数量,也是一种分段的思想 不过是放到了上面
        // 尝试使用CAS更新baseCount失败
        // CAS更新失败转用CounterCells进行更新
        /**
         * compareAndSwapObject(Object var1, long var2, Object var3, Object var4)
         * var1 操作的对象
         * var2 操作的对象属性
         * var3 var2与var3比较,相等才更新
         * var4 更新值
         * 例子:
         * UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
         */
        if ((cs = counterCells) != null || !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) { // 如果计数盒子不是空 或者如果修改baseCount 失败

            CounterCell c;
            long v;
            int m;

            boolean uncontended = true;

            // 如果计数盒子是空(尚未出现并发)
            // 如果随机取余一个数组位置为空 或者
            // 修改这个槽位的变量失败(出现并发了)
            // 执行 fullAddCount 方法。并结束

            if (cs == null || (m = cs.length - 1) < 0 ||  //如果cs为空 或者长度为0
                    (c = cs[ThreadLocalRandom.getProbe() & m]) == null || // 当前线程所拥有的cs为null
                      /*ThreadLocalRandom.getProbe() & m
                        每个线程都会通过ThreadLocalRandom.getProbe() & m寻址找到属于它的CounterCell,然后进行计数。
                        ThreadLocalRandom是一个线程私有的伪随机数生成器,每个线程的probe都是不同的,可以认为每个线程的probe就是它在CounterCell数组中的hash code。
                        这种方法将竞争数据按照线程的粒度进行分离,相比所有竞争线程对一个共享变量使用CAS不断尝试在性能上要效率多了,
                        这也是为什么在高并发环境下LongAdder要优于AtomicInteger的原因。
                       */
                    !(uncontended = U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))  //在当前线程的段上加数量失败
                    ) {

                // 强制增加数量(无论如何数量是一定要加上的,并不是简单地自旋)
                // 说明已经发生冲突了,那么就对counterCells进行扩容,以减少多个线程hash到同一个段的概率
                // 调用fullAddCount(),该函数负责初始化CounterCells和更新计数
                fullAddCount(x, uncontended);
                return;
            }

            // 这里check 可能在别的方法传入的时候传入了null
            if (check <= 1)
                return;

            // 计算元素个数
            s = sumCount();
        }

        // 如果需要检查,检查是否需要扩容,在 putVal 方法调用时,默认就是要检查的。
        if (check >= 0) {

            Node<K, V>[] tab, nt;
            int n, sc;

            // 如果元素个数达到了扩容门槛,则进行扩容
            // 注意,正常情况下sizeCtl存储的是扩容门槛,即容量的0.75倍
            while (s >= (long) (sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) {

                // 根据 length 得到一个标识
                int rs = resizeStamp(n);

                // 如果正在扩容
                if (sc < 0) {


                    // 如果 sc 的低 16 位不等于 标识符(校验异常 sizeCtl 变化了)
                    // 如果 sc == 标识符 + 1 (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
                    // 扩容已经完成了,退出循环 正常应该只会触发nextTable==null这个条件,其它条件没看出来何时触发
                    // 如果 sc == 标识符 + 65535(帮助线程数已经达到最大)
                    // 如果 nextTable == null(结束扩容了)
                    // 如果 transferIndex <= 0 (转移状态变化了)
                    // 结束循环
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0)
                        break;

                    // 如果可以帮助扩容,那么将 sc 加 1. 表示多了一个线程在帮助扩容
                    if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);

                // 如果不在扩容,将 sc 更新:标识符左移 16 位 然后 + 2. 也就是变成一个负数。高 16 位是标识符,低 16 位初始是 2.
                } else if (U.compareAndSetInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))

                    // 这里是触发扩容的那个线程进入的地方
                    // sizeCtl的高16位存储着rs这个扩容邮戳
                    // sizeCtl的低16位存储着扩容线程数加1,即(1+nThreads)
                    // 进入迁移元素
                    transfer(tab, null);

                // 重新计算元素个数
                s = sumCount();
            }
        }
    }
  • 变量解释
  • CounterCells数组:可以看成一个hash表结构(
    每个线程都会通过ThreadLocalRandom.getProbe() & m寻址找到属于它的CounterCell,然后进行计数。
    ThreadLocalRandom是一个线程私有的伪随机数生成器,每个线程的probe都是不同的,可以认为每个线程的probe就是它在CounterCell数组中的hash code,Value就是当前线程所存储的value的值)
  • 总结
  • (1)先尝试把数量加到baseCount上,如果失败再加到分段的CounterCell上
  • (2)元素个数存储在不同的段上,减少不同线程同时更新size时的冲突;
  • (3)计算元素个数时把这些段的值及baseCount相加算出总的元素个数;
  • (4)检查是否需要扩容
    • (4-1)如果正在扩容,当前线程帮忙一起扩容
    • (4-2)如果没有在扩容,则进行扩容
    •   (4-3)   重新计算元素数量

 

transfer(tab, nextTab);真正扩容的方法 扩容时容量变为两倍,并迁移部分元素

private final void transfer(Node<K, V>[] tab, Node<K, V>[] nextTab) {

        int n = tab.length, stride;

/***************************************【 初始化每个CPU要处理的链表的个数 】******************************************************************/

        //这里的目的是让每个 CPU 处理的桶一样多,避免出现转移任务不均匀的现象
        stride = (NCPU > 1) ? (n >>> 3) / NCPU : n;// 如果当前可同CPU的个数> 1  那么数组长度/8/cpu个数   如果cpu个数不大于1 那么1哥cpu处理全部的桶
        if (stride< MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE;
/***************************************【 初始化每个CPU要处理的链表的个数完毕 】******************************************************************/



/***************************************【 初始化nextTab 】******************************************************************/
        if (nextTab == null) {  // 如果nextTab为空,说明还没开始迁移
            try {
                @SuppressWarnings("unchecked")
                Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n << 1];  //就新建一个新桶数组 新桶数组是原桶的两倍
                nextTab = nt;  // 设置nextTab的值

            } catch (Throwable ex) {
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab; //更新成员变量

            transferIndex = n; //更新转移下标,表示转移时的下标(最一开始是数组的长度)
        }

/***************************************【 初始化nextTab完毕 】******************************************************************/



/***************************************【 初始化转移数组完毕 】******************************************************************/
        // 新桶数组大小
        int nextn = nextTab.length;

        /*创建一个 fwd 节点,表示一个正在被迁移的 Node,并且它的 hash 值为-1(MOVED),也
        //就是前面我们在讲 putval 方法的时候,会有一个判断 MOVED 的逻辑。它的作用是用来占位,表示
        //原数组中位置 i 处的节点完成迁移以后,就会在 i 位置设置一个 fwd 来告诉其他线程这个位置已经
        //处理过了,具体后续还会在讲*/
        // 表示正在被迁移的node
        ForwardingNode<K, V> fwd = new ForwardingNode<>(nextTab);

/***************************************【 初始化转移数组完毕 】******************************************************************/

        // 当前线程能否继续处理下一个链表
        boolean advance = true;

        // 扩容完成状态
        boolean finishing = false;


        // i 表示下标,   bound 表示当前线程可以处理的当前桶区间最小下标
        for (int i = 0, bound = 0;   ;     ) {
            Node<K, V> f;int fh;


            while (advance) {

                int nextIndex, nextBound;

                if (--i>= bound || finishing)// 上次分配给你的任务还没处理完
                    advance = false;

                else if ((nextIndex = transferIndex) <= 0) { //扩容结束了,当前线程可以退出了  就是hash桶没了
                    i = -1;
                    advance = false;

                    //你处理完了啊 再给你新的任务    获取最新的转移下标再去给我转一个区间
                } else if (U.compareAndSetInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { // CAS 修改 transferIndex,即 length - 区间值,留下剩余的区间值供后面的线程使用
                    bound = nextBound; // 当前线程可处理的链表区间的最小下标
                    i = nextIndex - 1;// 第一次赋值赋值的是当前线程可处理的链表区间的最大下标
                    advance = false;
                }
            }



            if (i < 0 || i >= n || i + n >= nextn) {  //目前只发现i<0这个条件

                int sc;
                if (finishing) {  // 扩容完毕
                    nextTable = null;
                    table = nextTab;  // 设置新数组
                    sizeCtl = (n << 1) - (n >>> 1);   // 并设置下一次扩容门槛为新桶数组容量的0.75倍
                    return;
                }



                if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    // 当前线程扩容完成,把扩容线程数-1
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) return;

                    // finishing为true才会走到上面的if条件
                    finishing = advance = true;
                    i = n;
                }

            } else if ((f = tabAt(tab, i)) == null)// 如果桶中无数据,直接放入ForwardingNode标记该桶已迁移
                advance = casTabAt(tab, i, null, fwd);

            else if ((fh = f.hash) == MOVED) // 如果桶中第一个元素的hash值为MOVED 说明它是ForwardingNode节点 也就是该桶已迁移
                advance = true;
            else {
                synchronized (f) {// 锁定该桶并迁移元素
                    if (tabAt(tab, i) == f) { //再次判断当前桶第一个元素是否有修改,也就是可能其它线程先一步迁移了元素

                        // 规则是桶中各元素的hash与桶大小n进行与操作
                        // 等于0的放到低位链表(low)中,不等于0的放到高位链表(high)中
                        // 其中低位链表迁移到新桶中的位置相对旧桶不变  高位链表迁移到新桶中位置正好是其在旧桶的位置加n

                        Node<K, V> ln, hn; //把一个链表分化成两个链表

                        if (fh >= 0) { //第一个元素的hash值大于等于0,说明该桶中元素是以链表形式存储的, 这里与HashMap迁移算法基本类似

                            int runBit = fh & n;
                            Node<K, V> lastRun = f;
                            for (Node<K, V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            // 看看最后这几个元素归属于低位链表还是高位链表
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            } else {
                                hn = lastRun;
                                ln = null;
                            }
                            // 遍历链表
                            for (Node<K, V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash;
                                K pk = p.key;
                                V pv = p.val;
                                if ((ph & n) == 0) //把hash&n为0的放在低位链表中
                                    ln = new Node<K, V>(ph, pk, pv, ln);
                                else //不为0的放在高位链表中
                                    hn = new Node<K, V>(ph, pk, pv, hn);
                            }
                            // 低位链表的位置不变
                            setTabAt(nextTab, i, ln);
                            // 高位链表的位置是原位置加n
                            setTabAt(nextTab, i + n, hn);
                            // 标记当前桶已迁移
                            setTabAt(tab, i, fwd);
                            // advance为true,返回上面进行--i操作
                            advance = true;
                        }
                        // 树节点不看
                        else if (f instanceof TreeBin) {
                            TreeBin<K, V> t = (TreeBin<K, V>) f;
                            TreeNode<K, V> lo = null, loTail = null;
                            TreeNode<K, V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K, V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K, V> p = new TreeNode<K, V>
                                        (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                } else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                    (hc != 0) ? new TreeBin<K, V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                    (lc != 0) ? new TreeBin<K, V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

关于这块的解释并发编程之 ConcurrentHashMap(JDK 1.8) putVal 源码分析写的就很详细啦

remove方法删除元素

   /**
     *  删除元素
     */
    public V remove(Object key) {
        // 调用替换节点方法
        return replaceNode(key, null, null);
    }

    /**
     *   替换元素
     */
    final V replaceNode(Object key, V value, Object cv) {
        // 计算hash
        int hash = spread(key.hashCode());

        // 自旋
        for (Node<K, V>[] tab = table; ; ) {
            Node<K, V> f;
            int n, i, fh;
            if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) & hash)) == null)   // 如果目标key所在的桶不存在,跳出循环返回null
                break;
            else if ((fh = f.hash) == MOVED)  // 如果正在扩容中,协助扩容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 标记是否处理过
                boolean validated = false;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {// 再次验证当前桶第一个元素是否被修改过
                        if (fh >= 0) {     // fh>=0表示是链表节点
                            validated = true; // 标记节点

                            for (Node<K, V> e = f, pred = null; ; ) { // 遍历链表寻找目标节点
                                K ek;
                                if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {       // 找到了目标节点
                                    V ev = e.val;
                                    if (cv == null || cv == ev || (ev != null && cv.equals(ev))) {    // 检查目标节点旧value是否等于cv
                                        oldVal = ev;
                                        if (value != null) // 如果value不为空则替换旧值
                                            e.val = value;
                                        else if (pred != null)  // 删除当前节点
                                            pred.next = e.next;
                                        else
                                            // 如果前置节点为空 说明是桶中第一个元素,删除之
                                            setTabAt(tab, i, e.next);
                                    }
                                    break;
                                }
                                pred = e; // 记录前一个节点
                                if ((e = e.next) == null)   // 遍历到链表尾部还没找到元素,跳出循环
                                    break;
                            }
                        } else if (f instanceof TreeBin) {
                            validated = true;
                            TreeBin<K, V> t = (TreeBin<K, V>) f;
                            TreeNode<K, V> r, p;
                            if ((r = t.root) != null &&
                                    (p = r.findTreeNode(hash, key, null)) != null) {
                                V pv = p.val;
                                if (cv == null || cv == pv ||
                                        (pv != null && cv.equals(pv))) {
                                    oldVal = pv;
                                    if (value != null)
                                        p.val = value;
                                    else if (t.removeTreeNode(p))
                                        setTabAt(tab, i, untreeify(t.first));
                                }
                            }
                        } else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }

                if (validated) {  // 如果处理过,不管有没有找到元素都返回
                    if (oldVal != null) {   // 如果处理过,不管有没有找到元素都返回
                        if (value == null)// 如果要替换的值为空,元素个数减1
                            addCount(-1L, -1);
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null;  // 没找到元素返回空
    }
  • (1)计算hash;
  • (2)如果所在的桶不存在,表示没有找到目标元素,返回;
  • (3)如果正在扩容,则协助扩容完成后再进行删除操作;
  • (4)如果是以链表形式存储的,则遍历整个链表查找元素,找到之后再删除;
  • (5)如果是以树形式存储的,则遍历树查找元素,找到之后再删除;
  • (6)如果是以树形式存储的,删除元素之后树较小,则退化成链表;
  • (7)如果确实删除了元素,则整个map元素个数减1,并返回旧值;
  • (8)如果没有删除元素,则返回null;

get方法获取元素

   /**
     *  获取元素的方法
     */
    public V get(Object key) {
        Node<K, V>[] tab;
        Node<K, V> e,p;
        int n, eh;
        K ek;
        int h = spread(key.hashCode());  // 计算hash

        if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {  // 如果元素所在的桶存在且里面有元素

            if ((eh = e.hash) == h) {    // 如果第一个元素就是要找的元素,直接返回
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;

            } else if (eh < 0)      // hash小于0,说明正在扩容
                return (p = e.find(h, key)) != null ? p.val : null; // 使用find寻找元素,find的寻找方式依据Node的不同子类有不同的实现方式
            while ((e = e.next) != null) {   // 遍历整个链表寻找元素
                if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }
  • (1)hash到元素所在的桶;
  • (2)如果桶中第一个元素就是该找的元素,直接返回;
  • (3)如果是树或者正在迁移元素,则调用各自Node子类的find()方法寻找元素;
  • (4)如果是链表,遍历整个链表寻找元素;
  • (5)获取元素没有加锁;

获取元素个数

   /**
     * 获取元素个数的方法
     * @return
     */
    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 : (n > (long) Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) n);
    }

    // 遍历CounterCell 线程中的数量  取得代表数量的value之后相加得出总数量 + sum(baseCount的值)

    final long sumCount() {

        CounterCell[] cs = counterCells;
        long sum = baseCount;
        if (cs != null) {
            for (CounterCell c : cs)
                if (c != null)
                    sum += c.value;
        }
        return sum;
    }
  • (1)元素的个数依据不同的线程存在在不同的段里;(见addCounter()分析)
  • (2)计算CounterCell所有段及baseCount的数量之和;
  • (3)获取元素个数没有加锁;

    参考自:微信公共号彤哥读源码

  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ConcurrentHashMap 是 Java 中的一个线程安全的哈希表,它是通过分段锁技术实现线程安全。下面对 ConcurrentHashMap源码进行简要解析。 ### 数据结构 ConcurrentHashMap 内部维护了一个 Segment 数组,每个 Segment 都是一个独立的哈希表,而且这些哈希表的数量可以在创建 ConcurrentHashMap 时指定。每个 Segment 内部都是一个类似 HashMap 的数据结构,也就是一个数组加链表的结构。ConcurrentHashMap 中的所有操作都是先定位到对应的 Segment,然后在 Segment 中进行操作。 ### put 方法 ConcurrentHashMap 的 put 方法首先会调用 hash 方法计算键的哈希值,然后根据哈希值找到对应的 Segment。接着会调用 Segment 的 put 方法,这个方法会加锁并且调用内部的 put 方法将键值对放入内部的 HashMap 中。如果 put 时,HashMap 中已经存在了这个键值对,那么就会更新这个键值对的值。最后释放锁。 ### get 方法 ConcurrentHashMap 的 get 方法也是先定位到对应的 Segment,然后调用内部的 get 方法,在内部的 HashMap 中查找键对应的值。由于在查找的过程中没有加锁,所以在多线程的情况下可能会出现一些数据不一致的问题,但是这个问题被认为是可以接受的,因为它不会影响数据的正确性。 ### size 方法 ConcurrentHashMap 的 size 方法也是先定位到对应的 Segment,然后调用内部的 count 方法,这个方法返回的是当前 Segment 中键值对的数量。最后将所有 Segment 中的键值对数量相加得到 ConcurrentHashMap 的大小。 ### 总结 ConcurrentHashMap 是通过分段锁技术实现线程安全的哈希表,它的内部维护了一个 Segment 数组,每个 Segment 都是一个独立的哈希表。ConcurrentHashMap 中的所有操作都是先定位到对应的 Segment,然后在 Segment 中进行操作。在 put 操作的过程中会加锁,而在 get 操作的过程中不会加锁,所以在多线程的情况下可能会出现一些数据不一致的问题,但是这个问题被认为是可以接受的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值