ConcurrentHashMap 1.8 计算 size 的方式

一、认识相关字段

相关字段,

    // 两种情况
    // 1. counterCells 数组未初始化,在没有线程争用时,将 size 的变化写入此字段
    // 2. 初始化 counterCells 数组时,没有获取到 cellsBusy 锁,会再次尝试将 size 的变化写入此字段
    private transient volatile long baseCount;
    
    // 用于同步 counterCells 数组结构修改的乐观锁资源
    private transient volatile int cellsBusy;

    // counterCells 数组一旦初始化,size 的变化将不再尝试写入 baseCount
    // 可以将 size 的变化写入数组中的任意元素
    // 可扩容,长度保持为 2 的幂
    private transient volatile CounterCell[] counterCells;

其中,CounterCell 是 ConcurrentHashMap 的一个静态内部类。

    /* ---------------- Counter support -------------- */

    /**
     * A padded cell for distributing counts.  Adapted from LongAdder
     * and Striped64.  See their internal docs for explanation.
     */
    @sun.misc.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }

二、计算 size 的源码分析

计算 size 的方法调用链:size() -> sumCount(),

    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n); // 将 n 裁剪到 [0, Integer.MAX_VALUE] 内
    }
    
    // 计算 baseCount 字段与所有 counterCells 数组的非空元素的和
    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;
    }
    

可以看到,map 中键值对的个数通过求 baseCount 与 counterCells 非空元素的和得到。

那么现在的问题就是 baseCount 和 counterCells 里的值都是什么时候计算的呢?

ConcurrentHashMap 内部所有改变键值对个数的方法都会调用 addCount 方法更新键值对的计数。
addCount 方法源码,

    // 参数 x 表示键值对个数的变化值,如果为正,表示新增了元素,如果为负,表示删除了元素
    private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        // 如果 counterCells 为空,则直接尝试通过 CAS 将 x 累加到 baseCount 中
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            // counterCells 非空
            // 或 counterCells 为空,但 CAS baseCount 失败都会来到这里
            CounterCell a; long v; int m;
            boolean uncontended = true; // CAS 数组元素时,有没有发生线程争用的标志
            // 如果当前线程探针哈希到的数组元素非空,则尝试将 x 累加到对应数组元素
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                // counterCells 为空,或其长度小于1
                // 或当前线程探针哈希到的数组元素为空
                // 或当前线程探针哈希到的数组元素非空,但 CAS 数组元素失败
                // 都会调用 fullAddCount 方法来完成 x 的写入
                fullAddCount(x, uncontended);
                return; // 如果调用过 fullAddCount,则当前线程一定不会协助扩容
            }
            // 走到这说明,CAS 数组元素成功
            // 此时如果 check <= 1,也不协助可能会发生的扩容
            if (check <= 1)
                return;
            // 如果 check 大于 1,则计算当前 map 的 size,为判断是否需要扩容做准备
            s = sumCount();
        }
        // size 的变化已经写入完成
        // 后面如果 check >= 0,则判断当前的 size 是否会触发扩容
        if (check >= 0) {
            // 扩容相关的逻辑
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

如果不管其中与扩容有关的逻辑,addCount 方法记录 size 变化的过程可以分为两类情况,

  1. counterCells 数组未初始化
    a. CAS 一次 baseCount
    b. 如果 CAS 失败,则调用 fullAddCount 方法

  2. counterCells 数组已初始化
    a. CAS 一次当前线程探针哈希到的数组元素
    b. 如果 CAS 失败,则调用 fullAddCount 方法

fullAddCount 方法的源码,

    // 只被 addCount 方法调用
    // 如果 counterCells 数组未初始化
    // 或者线程哈希到的 counterCells 数组元素未初始化
    // 或者 CAS 数组元素失败,都会调用此方法
    private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
        // 判断线程探针哈希值是否初始化
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true; // 重新假设未发生争用
        }
        boolean collide = false;                // 是否要给 counterCells 扩容的标志
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            if ((as = counterCells) != null && (n = as.length) > 0) {
                // 数组不为空且长度大于 0
                if ((a = as[(n - 1) & h]) == null) {
                    // 尝试初始化线程探针哈希到的数组元素
                    if (cellsBusy == 0) {            // Try to attach new Cell
                        // 注意,这里已经把 x 放入对象
                        CounterCell r = new CounterCell(x); // Optimistic create
                        if (cellsBusy == 0 && // 准备初始化数组元素,要求 cellsBusy 为 0,并尝试将其置 1
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            // 获得 cellsBusy 锁
                            boolean created = false;
                            try {               // Recheck under lock
                                CounterCell[] rs; int m, j;
                                // 判断有没有被其它线程初始化
                                if ((rs = counterCells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0; // 释放 cellsBusy 锁
                            }
                            if (created) // 初始化元素成功,直接退出循环
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash(指的是更改当前线程的探针哈希值)
                // wasUncontended 为 true 执行到这
                // 尝试将 x 累加进数组元素
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                // CAS 失败
                // 判断 counterCells 是否正在扩容,或数组长度是否大于等于处理器数
                else if (counterCells != as || n >= NCPU)
                    collide = false;            // At max size or stale
                // 如果数组没有在扩容,且数组长度小于处理器数
                // 此时,如果 collide 为 false,则把它变成 true
                // 在下一轮循环中,如果 CAS 数组元素继续失败,就会触发 counterCells 扩容
                else if (!collide)
                    collide = true;
                // 如果 collide 为 true,则尝试给 counterCells 数组扩容
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        if (counterCells == as) {// Expand table unless stale
                            CounterCell[] rs = new CounterCell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            counterCells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                h = ThreadLocalRandom.advanceProbe(h); // 更改当前线程的探针哈希值
            }
            // counterCells 数组为空或长度为 0
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                // 获取 cellsBusy 锁
                boolean init = false;
                try {                           // Initialize table
                    if (counterCells == as) {
                        CounterCell[] rs = new CounterCell[2]; // 初始长度为 2
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            // counterCells 数组为空或长度为 0,并且获取 cellsBusy 锁失败
            // 则会再次尝试将 x 累加到 baseCount
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        } // end for
    }

这个方法细节较多,比较复杂。
细节方面请参考上面的源码和注释。
下面,我们只从整体上看它实现了哪些功能,

  1. 线程探针哈希值的初始化。
  2. counterCells 数组的初始化和扩容。
  3. counterCells 元素的初始化。
  4. 将 size 的变化,写入 counterCells 中的某一个元素。(如果 counterCells 初始化时,获取锁失败,则还会尝试将 size 的变化,写入 baseCount。)

三、小小总结

代码虽然看起来很复杂,但 Doug Lea 计算 size 的思想很明确,也很巧妙。

指导思想: 尽量降低线程冲突,以最快的速度写入 size 的变化。

如何降低冲突?
如果没有冲突发生,只将 size 的变化写入 baseCount。
一旦发生冲突,就用一个数组(counterCells)来存储后续所有 size 的变化。这样,线程只要对任意一个数组元素写入 size 变化成功即可,数组长度越长,线程发生冲突的可能性就越小。

关于 counterCells 扩容:
如果 CAS 数组元素连续失败两次,就会进行 counterCells 数组的扩容,直到达到机器的处理器数为止。
比如我的机器是双核四线程,真正能并行的线程数是 4,所以在我机器上 counterCells 初始化后,最多扩容一次。

关于线程的探针哈希值是如何初始化和更改的,可以参考:关于 ConcurrentHashMap 1.8 中的线程探针哈希

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266