java-JDK1.8 concurrenthashmap 源码阅读

本文部分照片和代码分析来自文末参考资料

java8中的concurrenthashmap的方法逻辑和注解有些问题,建议看最新的JDK版本

建议阅读 concurrenthashmap 源码前过一遍源码前面的注释,参考资料第二篇的博客有注释翻译

问题

  • ConcurrentHashMap了解实现原理、扩容时做的优化、与HashTable对比。
  • ConcurrentHashMap是如何在保证并发安全的同时提高性能?
  • ConcurrentHashMap是如何让多线程同时参与扩容?
  • 了解ConcurrentHashMap吗?说一下它为什么能线程安全
  • size 方法是否阻塞 , get 方法刚好在迁移的时候是否也会阻塞 ?

概述

来自参考资料

说明:因为ConcurrentHashMap单词太长,所以下面均适用CHM替代ConcurrentHashMap

  • 同为线程安全集合,但CHM没有任何访问操作需要锁定全表。这也注定了CHM上的操作效率之高。
  • 表访问需要volatile/atomic读,写和CAS.这通过使用内在函数(sun.misc.Unsafe)完成。
  • 向一个空bin中插入节点是通过CAS完成的,其它的更新操作(insert,delete,update)都需要锁lock。
  • CHM的key,value都不能为null
  • 访问操作(get等)和更新操作(remove等)同时发生时,根据happens-before原则,更新操作先执行,读操作后执行,从而保证了检索操作获取的值一定是最新值。
  • 聚合状态方法的结果包括:size,isEmpty,containsValue通常都是仅当map不在其它线程中进行并发更新时才有用。
  • 批量操作可以接受一个并行阈值参数parallelismThreshold。
  • 如果当前map的size预估值比给定的阈值小,则方法顺序执行。
  • 如果给定阈值=Long.MAX_VALUE,则不会出现并行操作。
  • 如果给定阈值=1,则会导致并行最大化,通过使用ForkJoinPool.commonPool()方法,对子任务分离。
  • 并行操作通常比顺序操作快,但不能保证一定是这样。并行操作更慢的情况有:
  • 如果并行计算的基础工作比计算本身更昂贵,那么小map上的并行操作可能比顺序形式执行更慢。
  • 如果所有处理器都忙于执行不相关的任务,并行化可能无法实现太多的实际并行性。(无法形成流水线操作)
  • 支持序列化,不支持浅拷贝
  • 两个线程访问同一个bin中不同的元素的锁争用概率为:1 / (8 * #elements)
  • TreeBins存在的意义:保护了我们免于因过度resize带来的最坏影响。
  • 每一个bin中的元素到达新的bin后要么索引不变,要么产生2的次幂的位移。我们通过捕获旧节点可以重用的情况来消除不必要的节点创建。平均而言,当table进行resize时,只有1/6的节点需要进行clone。
  • table进行resize时,其它线程可以加入协助resize的过程(这不是为了获取锁),从而使得平均聚合等待时间变短。
  • 在遇到转发节点时,遍历会移动到新table而无需重新访问节点
  • TreeBins也需要额外的锁定机制。list更新过程中依旧可以进行遍历,但是红黑树在更新时却不能进行遍历,因为红黑树的调整可能会改变树的根节点,也可能改变各个节点之间的连接情况。
  • TreeBins包含一个简单的读写锁定机制,依赖于主要的同步策略:
    插入,删除的结构调整会调用lock机制;
  • 如果在结构调整前有读操作,则必须读操作完成后,再进行结构的调整操作。遵循happes-before原则。
    扩展AbstractMap,但这只是仅仅为了与这个类的以前版本兼容。

能够用TreeMap替代TreeBin?

不能, 原因:TreeBins的查询及与查询相关的操作都使用了一种特殊的比较形式。TreeBins中包含的元素可能在实现Comparable上的原则不一样,所以对于它们之间的比较,则无法调用CompareTo()方法。为了解决这一问题,tree通过hash值对其排序。如果Comparable.compareTo 可用的话,再用这个方法对元素排序。在查找节点时,如果元素不具有可比性或比较为0,则可能需要对此节点对左右孩子都进行查询。如果所有元素都不是可比较的并且具有相同的哈希值,则需要对全table进行扫描。

从JDK8开始,CHM使用CAS算法替代了Segment的概念,保证线程安全。
-- 构成结构:bin数组+链表+红黑树
-- 红黑树被包装在TreeBin内
-- 扩容机制:2倍扩容
常量:
默认容量:16;
负载因子:0.75
说明:在构造函数中重写此值只会影响初始表的容量,而不会使用实际的浮点值。
链表转红黑树阈值:8
红黑树转链表阈值:6
table转为红黑树阈值:64
resize时的最小并行度:16(因为默认16个bin)

java8 的 concurrenthashmap 主要工作方式如下 :

howtowork

可以看到总体就是链表+红黑树。当链表数量达到一定值(默认是8)时,我们就会将链表转化为红黑树。concurrenthash的特点就是并发,那么是如何并发的呢?下面是几个concurrenthash 的工作过程。

源码阅读

变量

/* ---------------- Fields -------------- */

/**
 * 底层数组
 */
transient volatile Node<K,V>[] table;
//扩容时 使用的另一个数组
private transient volatile Node<K,V>[] nextTable;

//统计size的一部分
private transient volatile long baseCount;

/**
 * sizeCtl与table的resize和init有关
 * sizeCtl = -1时,表示table正在init
 * sizeCtl < 0 且不等于-1时,表示正在resize
 * sizeCtl > 0 时,表示下次需要resize的阈值,即capacity * loadfactory , 例如 初始化的时候 ,table 默认大小为 16,sizeCtl 被设置为 12 
 */
private transient volatile int sizeCtl;

//记录下一次要transfer对应的Index
private transient volatile int transferIndex;
//表示是否有线程正在修改CounterCells
private transient volatile int cellsBusy;

//用来统计size
private transient volatile CounterCell[] counterCells;

// views
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
  • transferIndex : 指示目前迁移到哪了

  • stride : (这个是在transfer方法的局部变量,拿出来说明,是为了下文更好地理解transfer)假如一个迁移任务分为多个小迁移任务,一个位置为一个任务单位,小迁移任务中包含多个任务,stride代表小迁移任务中的任务数量。最小的stride是I6,最小的数组长度也是16,就意味着迁移任务分为1个小迁移任务,小迁移任务中有16个任务。

  • basecount : 统计数量的时候用到

  • cellsBusy : 基于 CAS 的自旋锁 ,当创建 CounterCells 或调整大小的时候使用

  • CounterCell[] counterCells : 统计数量的时候用到

常量

/* ---------------- Constants -------------- */

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

/**
 * 默认容量
 */
private static final int DEFAULT_CAPACITY = 16;

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

/**
 * 默认最大并发等级
 */
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

/**
 * 负载因子
 */
private static final float LOAD_FACTOR = 0.75f;

/**
 * 链表升级成红黑树的阈值
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 红黑树退化成链表的阈值
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 链表升级成树需要满足的最小容量,若不满足,则会先扩容
 */
static final int MIN_TREEIFY_CAPACITY = 64;

//最小转移步长
private static final int MIN_TRANSFER_STRIDE = 16;

//这个常量是用来计算HashMap不同容量有不同的resizeStamp用的
private static int RESIZE_STAMP_BITS = 16;
//最大参与扩容的线程数 相当大的一个数 基本上是不会触及该上线的
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

//要对resizeStamp进行位移运算的一个敞亮
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

//特殊的节点哈希值
static final int MOVED     = -1; // hash for forwarding nodes
static final int TREEBIN   = -2; // hash for roots of trees
static final int RESERVED  = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

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

put 方法

    public V put(K key, V value) {
        return putVal(key, value, false);
    }



    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //计算 hash 值  
        int hash = spread(key.hashCode());
        int binCount = 0;
        //for 循环 , 除非遇到 break , 或是 return 
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 首次加入或是 tab 里面没元素
            if (tab == null || (n = tab.length) == 0)
                tab = initTable(); //初始化 Table,然后继续运行,到for里面继续循环
            //发现在列表中没有
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            	//CAS 放进去,成功后 break退出循环
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //在列表中存在,hash 值为 MOVED(-1)
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                //进到这里 f , fh 分别都被赋值了,列表中存在该值
                V oldVal = null;
                //直接加锁操作
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        //如果为链表 , 为什么 fh >= 0 是链表 ,是因为红黑树的根hash值为非正数
                        if (fh >= 0) {
                            binCount = 1;
                            // for 循环整个链表
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //找到,更新
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                //找不到,e指向e后面那个
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    //最后这条链表没有,就加在后面
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //如果为红黑树 ,插入里面
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                //存在一样的 key , 更新
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // binCount 表示这个槽位有的节点有多少个
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        //判断是否应该变为红黑树,treeifyBin 也有可能是扩展了数组大小。 
                        // 也就是说每当有一个链表的数量大于 8 的时候就会进到这里来 
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 总数 + 1 
        addCount(1L, binCount);
        return null;
    }

初始化数组

    /**
     * Initializes table, using the size recorded in sizeCtl.
     *
     * 这里假如有多个线程执行put 操作,那么初始化线程的操作应该只能执行一次
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            //CAS 将 sizeCtl 赋值为 -1 ,那么CAS失败的线程进行循环要不让步,要不就tab得到赋值退出 
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        //这里就是 0.75*n 
                        sc = n - (n >>> 2);
                    }
                } finally {
                    // sizeCtl = sc = 0.75*n 
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

Thread.yield()方法的作用是让当前线程暂停执行,让出CPU资源给其他线程。调用Thread.yield()方法后,当前线程会进入就绪状态,等待CPU重新调度执行。
Thread.yield()方法并不是强制性的,它只是对线程调度器的一个建议,告诉线程调度器当前线程已经完成了一些重要的工作,可以让出CPU资源给其他线程执行。但是,线程调度器不一定会采纳这个建议,因此Thread.yield()方法并不能保证当前线程一定会让出CPU资源。

统计数量

    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }

    final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

就是baseCount + 各数组之和 , 那么 CounterCell[] 到底是什么呢 ? 它的原理是什么呢 ? 从 CounterCell 这类上的注释上看到 :
该类改编自 LongAdderStriped64 , 我们思考一下 ,我们统计数量其实也可以使用 AtomicLong 这样子的原子类去统计, 但是 concurrenthashmap
追求更高的性能 , 用 LongAdder 来实现 , LongAdder 的实现原理如下 :

img

这里就不深入源码去理解这个过程了.

扩容和迁移

假如上面的put 方法进入了treeifyBin 方法,那么接下来要进行扩容。

迁移有几个要点需要理解一下

  • 迁移过程分为 : 1. 邻取迁移任务 2. 执行迁移任务
  • 迁移过程从后往前 , 以最大限度避免遍历时冲突 , 遍历是从前往后遍历
  • 当数组大小小于64 , 就先会先扩容迁移 ,而不是马上就变成红黑树
  /**
     * Replaces all linked nodes in bin at given index unless table is
     * too small, in which case resizes instead.
     */
    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n;
        if (tab != null) {
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                //扩容迁移
                tryPresize(n << 1);
            //生成红黑树
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }





    /**
     * Tries to presize table to accommodate the given number of elements.
     *
     * @param size number of elements (doesn't need to be perfectly accurate)
     */
    private final void tryPresize(int size) {
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            //和initTable一样
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            else if (tab == table) {
                //扩容迁移
                int rs = resizeStamp(n);
                if (U.compareAndSetInt(this, SIZECTL, sc,
                                        (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }

链表树化头结点会是一个特殊的 Node --- TreeBin , 这种node 里面存在着一个类似于读写锁的成员变量 , 所以它的 find 方法 有可能会阻塞 , 而它的 find方法在 concurrentHashMap 的 get 方法有所体现 .

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;

    // stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16
    // stride 可以理解为”步长“,有 n 个位置是需要进行迁移的,
    //   将这 n 个任务分为多个任务包,每个任务包有 stride 个任务
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range

    // 如果 nextTab 为 null,先进行一次初始化
    //    前面我们说了,外围会保证第一个发起迁移的线程调用此方法时,参数 nextTab 为 null
    //       之后参与迁移的线程调用此方法时,nextTab 不会为 null
    if (nextTab == null) {
        try {
            // 容量翻倍
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        // nextTable 是 ConcurrentHashMap 中的属性
        nextTable = nextTab;
        // transferIndex 也是 ConcurrentHashMap 的属性,用于控制迁移的位置
        transferIndex = n;
    }

    int nextn = nextTab.length;

    // ForwardingNode 翻译过来就是正在被迁移的 Node
    // 这个构造方法会生成一个Node,key、value 和 next 都为 null,关键是 hash 为 MOVED
    // 后面我们会看到,原数组中位置 i 处的节点完成迁移工作后,
    //    就会将位置 i 处设置为这个 ForwardingNode,用来告诉其他线程该位置已经处理过了
    //    所以它其实相当于是一个标志。
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

    // 注意一个批次里面有多个槽位 , 这两个表示的东西不一样 
    // advance 指的是做完了一个槽位的迁移工作,是否可以进行下一个槽位的搬运 
    // finishing 指的是迁移是否已经完成
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab

    /*
     * 下面这个 for 循环,最难理解的在前面,而要看懂它们,应该先看懂后面的,然后再倒回来看
     *
     */

    // i 是位置索引,bound 是边界,注意是从后往前
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;

        // 下面这个 while 真的是不好理解
        // advance 为 true 表示可以进行下一个位置的迁移了
        //   简单理解结局:i 指向了 transferIndex,bound 指向了 transferIndex-stride
        //   下面这个while 的作用是 i 和 bound 进行 赋值 (为每个线程划定任务区)
        while (advance) {
            int nextIndex, nextBound;

            /**
             * --i >= bound  条件成立就表示进行下一个桶迁移 ,减一
             *  finishing    该批次迁移完成啦!!
             */
            if (--i >= bound || finishing)
                advance = false;

            // 将 transferIndex 值赋给 nextIndex
            // 这里 transferIndex 一旦小于等于 0,说明原数组的所有位置都有相应的线程去处理了
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            //一次小迁移任务完成后(此时还有得分配),迁移的线程继续来到这里,和想帮忙的线程一起抢
            else if (U.compareAndSetInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                // 看括号中的代码,nextBound 是这次迁移任务的边界,注意,是从后往前
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
            //抢不到就继续循环,直到退出
        }

        //这个if里面的作用是对所有的迁移操作是否完成进行判断 (判断所有任务是否 OK ),或是从 上面第二个 else if 过来的 
        /**
         *   n : 原数组槽位数
         *   nextn : 新数组槽位数   
         * 
         *   1. i < 0 最后一批次的位置,即原数组的第一个元素都迁移完了, 因为是从后往前迁移的,所以意味着结束迁移任务分配完成 ,(此时有可能还在搬运, 只是不需要其他线程协助搬运了); 也可能是上面 while 里面的第二个 `else if` i 被赋值了 -1 
         *   2. i >= n   代码A处 i = n  ,表示迁移完成了 
         *   3. i + n >= nextn  
         * 
         * 
         * */

        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                // 所有的迁移操作已经完成
                nextTable = null;
                // 将新的 nextTab 赋值给 table 属性,完成迁移
                table = nextTab;
                // 重新计算 sizeCtl:n 是原数组长度,所以 sizeCtl 得出的值将是新数组长度的 0.75 倍
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }

            // 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
            // 然后,每有一个线程参与迁移就会将 sizeCtl 加 1,
            // 这里使用 CAS 操作对 sizeCtl 进行减 1,代表做完了属于自己的任务
            if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    // 进来这里就是说明这一个批次的这个线程迁移完成,其他线程还在帮忙迁移没有完成 ,所以 return 就行了
                    //(假如一个次迁移完成,没人帮忙,等号两边应该是相等的)
                    return;

                //最后一批迁移任务完成
                // 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
                // 也就是说,所有迁移线程都退出了(即是迁移任务完成了),也就会进入到上面的 if(finishing){} 分支了
                finishing = advance = true;
                i = n; // recheck before commit   代码A处
            }
        }

        //下面的 else if 是对分到的任务区进行判断,是否已经迁移过了(是否迁移过的标志就是 ForwardingNode)
        //要是都不是,那么就是还没迁移咯,开始自己任务区的迁移工作!!

        // 如果位置 i 处是空的,没有任何节点,那么放入刚刚初始化的 ForwardingNode ”空节点“,
        //表明这个区域正在迁移
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        // 该位置处是一个 ForwardingNode,代表该位置已经迁移过了
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            // 对数组该位置处的结点加锁,开始处理数组该位置处的迁移工作
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    // 头结点的 hash 大于 0,说明是链表的 Node 节点 ? 这是为啥  
                    // 
                    if (fh >= 0) {
                        // 下面这一块和 Java7 中的 ConcurrentHashMap 迁移是差不多的,
                        // 需要将链表一分为二,
                        //   找到原链表中的 lastRun,然后 lastRun 及其之后的节点是一起进行迁移的
                        //   lastRun 之前的节点需要进行克隆,然后分到两个链表中
                        //  文档中说到大概只有六分之一需要复制
                        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)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        // 其中的一个链表放在新数组的位置 i
                        setTabAt(nextTab, i, ln);
                        // 另一个链表放在新数组的位置 i+n
                        setTabAt(nextTab, i + n, hn);
                        // 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
                        // 他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
                        setTabAt(tab, i, fwd);
                        // advance 设置为 true,代表该位置已经迁移完毕,进行下一个循环
                        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;
                            }
                        }
                        // 如果一分为二后,节点数少于 8,那么将红黑树转换回链表
                        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;

                        // 将 ln 放置在新数组的位置 i
                        setTabAt(nextTab, i, ln);
                        // 将 hn 放置在新数组的位置 i+n
                        setTabAt(nextTab, i + n, hn);
                        // 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
                        //    其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
                        setTabAt(tab, i, fwd);
                        // advance 设置为 true,代表该位置已经迁移完毕
                        advance = true;
                    }else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                }
            }
        }
    }
}

sizeCtl 为负数的时候后16位被用作数据迁移的线程 .

    /**
     * Returns the stamp bits for resizing a table of size n.
     * Must be negative when shifted left by RESIZE_STAMP_SHIFT.
     *
     * Integer.numberOfLeadingZeros(n) 返回的是 n 前面有多少个零
     *
     */
    static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }

这里可能对 sizeCtl 的移位判断难以理解, 下面我写了一段用例 , 应该就很好理解了

@Slf4j
public class ChashMapTest {

    private static int RESIZE_STAMP_BITS = 16;
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;


    static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }

    public static void main(String[] args) {

        for (int n = 1; n > 0; n *= 2) {
            /**
             * int rs = resizeStamp(n);
             *             int ans = (rs << RESIZE_STAMP_SHIFT) + 2;
             */
            int nd = (n << 1) - (n >>> 1);

            int rs = resizeStamp(n);
            int sc =( rs << RESIZE_STAMP_SHIFT )+ 2;
            String rsb = Integer.toBinaryString(resizeStamp(n));
            String scb =Integer.toBinaryString(( rs << RESIZE_STAMP_SHIFT )+ 2);
            log.info("n: {} , rs :{} , sc:{} , rsb :{} , scb:{} , nd:{}", n, rs, sc,rsb,scb,nd );
        }


    }

其中 rsbscb是前面两个变量的二进制表示

n: 1 , rs :32799 , sc:-2145452030 , rsb :1000000000011111 , scb:10000000000111110000000000000010 , nd:2
n: 2 , rs :32798 , sc:-2145517566 , rsb :1000000000011110 , scb:10000000000111100000000000000010 , nd:3
n: 4 , rs :32797 , sc:-2145583102 , rsb :1000000000011101 , scb:10000000000111010000000000000010 , nd:6
n: 8 , rs :32796 , sc:-2145648638 , rsb :1000000000011100 , scb:10000000000111000000000000000010 , nd:12
n: 16 , rs :32795 , sc:-2145714174 , rsb :1000000000011011 , scb:10000000000110110000000000000010 , nd:24
n: 32 , rs :32794 , sc:-2145779710 , rsb :1000000000011010 , scb:10000000000110100000000000000010 , nd:48
n: 64 , rs :32793 , sc:-2145845246 , rsb :1000000000011001 , scb:10000000000110010000000000000010 , nd:96
n: 128 , rs :32792 , sc:-2145910782 , rsb :1000000000011000 , scb:10000000000110000000000000000010 , nd:192
n: 256 , rs :32791 , sc:-2145976318 , rsb :1000000000010111 , scb:10000000000101110000000000000010 , nd:384
n: 512 , rs :32790 , sc:-2146041854 , rsb :1000000000010110 , scb:10000000000101100000000000000010 , nd:768
n: 1024 , rs :32789 , sc:-2146107390 , rsb :1000000000010101 , scb:10000000000101010000000000000010 , nd:1536
n: 2048 , rs :32788 , sc:-2146172926 , rsb :1000000000010100 , scb:10000000000101000000000000000010 , nd:3072
n: 4096 , rs :32787 , sc:-2146238462 , rsb :1000000000010011 , scb:10000000000100110000000000000010 , nd:6144
n: 8192 , rs :32786 , sc:-2146303998 , rsb :1000000000010010 , scb:10000000000100100000000000000010 , nd:12288
n: 16384 , rs :32785 , sc:-2146369534 , rsb :1000000000010001 , scb:10000000000100010000000000000010 , nd:24576
n: 32768 , rs :32784 , sc:-2146435070 , rsb :1000000000010000 , scb:10000000000100000000000000000010 , nd:49152
n: 65536 , rs :32783 , sc:-2146500606 , rsb :1000000000001111 , scb:10000000000011110000000000000010 , nd:98304
n: 131072 , rs :32782 , sc:-2146566142 , rsb :1000000000001110 , scb:10000000000011100000000000000010 , nd:196608
n: 262144 , rs :32781 , sc:-2146631678 , rsb :1000000000001101 , scb:10000000000011010000000000000010 , nd:393216
n: 524288 , rs :32780 , sc:-2146697214 , rsb :1000000000001100 , scb:10000000000011000000000000000010 , nd:786432
n: 1048576 , rs :32779 , sc:-2146762750 , rsb :1000000000001011 , scb:10000000000010110000000000000010 , nd:1572864
n: 2097152 , rs :32778 , sc:-2146828286 , rsb :1000000000001010 , scb:10000000000010100000000000000010 , nd:3145728
n: 4194304 , rs :32777 , sc:-2146893822 , rsb :1000000000001001 , scb:10000000000010010000000000000010 , nd:6291456

可以看到 + 2 以后其实就是后两位变成 10了 ,或者这样看 :
img

更加形象一点我们用以下的图片来介绍迁移的过程

图片来源 : https://juejin.cn/post/6896387191828643847

  • 多线程开始扩容

img

  • lastrun节点
    img

  • 链表迁移
    img

  • 红黑树迁移
    img

  • 迁移过程中get和put的操作的处理
    img

  • 并发迁移

img

  • 迁移完成

img

帮助迁移

    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                // sc == rs + 1 不知是什么意思
                if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                    transferIndex <= 0)
                    break;
                //到这里就表示条件满足了, CAS 后进入帮助迁移
                //可以知道 nextTab 此时不为空了,要是进入是空的话,方法内部也会创建
                if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }

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());
        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;
            }
            // eh < 0 ,如果是红黑树,去红黑树上查找,如果是 ForwardingNode ,则会跳到扩容后的 map 上寻找
            else if (eh < 0)
                //有可能是红黑树节点,或是其他节点,find 方法在每种节点有重写
                return (p = e.find(h, key)) != null ? p.val : null;
            // 遍历链表查找     
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

总结

文章介绍了 ConcurrentHashMap 的原理 , 不足之处请指出

参考资料:

posted @ 2019-01-11 15:56  float123  阅读(477)  评论(0)    收藏  举报