并发编程之ConcurrentHashMap源码分析

1. 主源码逻辑

java 复制代码
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        // 1.计算key对应的hash
        int hash = spread(key.hashCode());
        int binCount = 0;
        // 2. 进行自旋 
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 3. 首次进来的时候table为空
            if (tab == null || (n = tab.length) == 0)
                // 4. 进行初始化 此时没有任何锁机制 因此该初始化采用CAS机制,保证只有一个线程能 
                      够初始化完成,其它线程就可以直接使用该初始化的table
                tab = initTable();
            // 5.(n-1)&hash  -> 0-15  计算数组下标位置
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 6.如果当前位置为空,则采用cas操作保证原子性直接进行赋值(tabAt  casTabAt 都是设置的内存偏移量来取值和设置值的)
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 7. 当前节点正在进行table扩容和数据转移操作 则当前线程协助扩容和数据转移
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            // 8. 同步锁代码块 进行值的覆盖或添加
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 9.进行值的覆盖
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                // 10.进行值的添加 挂在链表下面
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        // 11. 若当前节点属于红黑树 则进行红黑树的添加
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // 12. 判断是否要进行扩容还是转红黑树
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 13.统计元素个数
        addCount(1L, binCount);
        return null;
    }

2. 首次put操作

假设有3个线程t1,t2,t3此时同时put,t1和t2的key完全相同,与t3的是hash相同,key不同,初次进来时,进行表的初始化,table的初始化位一个CAS操作,因此只有一个线程会进行初始化,另外两个线程此时会不断的自旋等待。

精简主逻辑代码

java 复制代码
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        // 首次自旋 初始化一个默认为16长度的table
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
        }

2.1 初始化table(initTable)

java 复制代码
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        // 4.1只要table没有初始化,就不断循环直到初始化完成
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            //4.2 通过CAS来占用一个锁的标记 sizeCTL = -1 
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                // 4.3 说明当前线程抢到了锁
                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;
                        sc = n - (n >>> 2);    // 4.4 保留扩容的阈值 12
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

分析: sizeCtl作为一个ConcurrentHashMap的控制标签,贯穿全局 未初始化时默认为0 初始化中 U.compareAndSwapInt(this, SIZECTL, sc, -1) 采用CAS操作保证其原子性,将sizeCtl更新为-1,表示当前table正在初始化中,初始化的table默认长度为16,其扩容阈值sizeCtl = 12

2.2 casTabAt赋值操作

精简主逻辑源码:

java 复制代码
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 线程竞争进行赋值
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
        }
        addCount(1L, binCount);
        return null;
    }

此时线程1 线程2 线程3 会同时进入到赋值操作,赋值为CAS操作,假设线程1抢占资源成功,则线程1中的key,value赋值到该table索引处,退出循环,执行addCount 统计元素个数; 线程2,线程3 在此处CAS失败,则会再次自旋,等线程1赋值成功后,走入下面的if判断中。

主逻辑源码精简:

java 复制代码
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        // a.代表该节点下是链表数据结构
                        if (fh >= 0) { 
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // b.进行值的覆盖
                                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;
                                // c.挂在链表后面
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

此时线程2和线程3 进入到a中,线程2的赋值进入到b的代码,线程3的赋值进入到c的代码,随后也进入到addCount中进行元素个数的统计

3.第N次put操作,链表下长度达到阈值TREEIFY_THRESHOLD

假设此时线程4,和线程5又同时进行put操作,且key值的hash计算出来的索引处链表已经达到了长度8,则此时,程4和线程5的值会先新增到原来的链表上,然后进入到代码treeifyBin(tab, i)中,判断是将table进行扩容还是转为红黑树

精简主逻辑代码

java 复制代码
                  if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        // 对table进行扩容或者将链表转为红黑树
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
java 复制代码
    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            // 判断当前table的长度是否达到了阈值64
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                // 如果没有,则进行扩容和数据转移操作
                tryPresize(n << 1);
        }
    }

此时table的长度为16,没有达到阈值64,则执行tryPresize,进行table的扩容和数据转移

3.1 table扩容和数据转移 tryPresize

java 复制代码
    private final void tryPresize(int size) {
        //1. 计算出一个最大容量,判断table是否已经达到了最大容量
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        //2. 此时sizeCtl = 12(前面的阈值)
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            //3. 如果table没有初始化,则进行初始化
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                if (U.compareAndSwapInt(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;
                    }
                }
            }
            // 4.判断是否已经达到了最大阈值,如果已经达到,则不再允许扩容
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            else if (tab == table) {
                // 5.计算出一个扩容戳,作为扩容标记,保证当前扩容的唯一性 此时n为16,则生成的扩容戳则为 1000 0000 0001 1001
                int rs = resizeStamp(n);
                // 6.判断当前是否已经有线程在进行扩容和数据转移操作了
                //  因为sc = sizeCtl,而sizeCtl如果有线程在进行transfer操作,则会变为负数
                if (sc < 0) {
                    Node<K,V>[] nt;
                    // 假设当前已经有一个线程在进行操作了,则此时 
                    // 第一个条件: sc = 1000 0000 0001 1001 0000 0000 0000 0010 
                    // rs = 1000 0000 0001 1001 sc右移 16位时与rs相等,故为false
                    // 第二、三个条件也位为false   当有线程在操作时nextTable不为空, 
                    // transferIndex也不为空
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    // 当前transfer操作未结束,则加入协助,SIZECTL -> 1000 0000 0001 1001 
                    // 0000 0000 0000 0011 在低16位上加1 
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                // 首次执行该逻辑 将SIZECTL 12->  1000 0000 0001 1001 0000 0000 0000 0010 // 其中高16位代表当前操作的唯一性,低16位表示当前正在参与transfer操作的线程数
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }

分析:

3.2 多线程协助数据转移transfer

主要做两件事: 1.table的扩容;2.把旧数据从旧table转移到新table中

transfer代码拆解分析:

3.2.1 table扩容

新table的扩容,由原来的16扩容到32,并用 nextTable记录新Table,transferIndex 记录旧table的长度

java 复制代码
   private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        // 1.旧table长度n
        int n = tab.length, stride;
        // 2.计算线程进行数据操作的区间,默认为16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        // 3. 扩容后table的初始化 16 -> 32
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                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;
            }
            // 4. 记录新table
            nextTable = nextTab;
            // 5. 记录旧table的长度,也可以说是线程转移的索引值
            transferIndex = n;
        }
    }

3.2.2 数据转移

java 复制代码
   private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        // 1. 记录新table的长度 32
        int nextn = nextTab.length;
        // 2. 一个占位符,表示当前节点正在迁移中, 该节点的hash值为MOVED -1
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        // 3.循环迁移 迁移的区间为[bound,i]
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
           // 4. 计算出迁移的区间 [bound,i]
            while (advance) {
                int nextIndex, nextBound;
                // 4. 表示迁移完成 退出循环
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                // 5.CAS操作, 计算迁移区间 此时stride = 16,nextIndex = 16,所以最后计算出来的 
                //    期间为[0,15]
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
             // 6. 判断数据是否迁移完成
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                // 7. 数据迁移完成,赋值新table
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
               // 8. cas操作 更新 SIZECTL 减去一个线程 
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    // 9.当所有线程都完成后,进行return
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            // 10.进行转移操作,当前索引处为null值时,标记为MOVED
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
           // 11. 如果当前节点的hash为MOVED,表示当前节点已经迁移了
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                // 12. 进入同步代码块
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        // 13.记录链表的高低位,进行高低位迁移
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            // 14. 记录当前数据是否需要迁移 若为1则表示需要迁移,若为0则不需要 
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            // 15. 循环遍历整个链表,进行hash校验
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            // 16.记录下低位 ln
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            // 17. 记录高位 hn
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            // 18. 计算出哪些需要迁移hn 哪些不需要迁移ln 
                            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);
                            }
                            // 19.将不需要迁移的仍挂在旧table的该处索引下
                            setTabAt(nextTab, i, ln);
                            //20. 需要迁移的放在新的table中,其间隔为16
                            setTabAt(nextTab, i + n, hn);
                            //21. 更新此处的标记 为MOVED
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

分析: 上述代码中第5处的CAS操作,若原数组有64个,则第一个线程进来的时候进行该CAS操作,获得的线程区间就是[47,63],第二个线程再次进来的时候获的数据转移区间就是[31,47]依次类推

确定好数据转移区间后,一种是链表数据的迁移,另一种是红黑树数据的迁移,红黑树数据的迁移在此不进行展开

链表的迁移主要采用的是高低链位迁移法.

具体是怎么做的呢? 为什么fh & n为1就是高位需要迁移了,为0就不需要迁移了。 以4和20举例,原数组长度16计算出来的索引为 4&15=4 20 &15= 4 而15变为二进制是为00001111,当数组长度变为32时, 计算的索引为 4& 31 = 4 20&31 =20 31为00011111,而此时fh&n n 为16,高位为1,4& 16为0则为低位链, 20& 16就为高位链,于是就区分开了低位链和高位链。

4.元素个数的统计

java 复制代码
    private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        // 1. 个数的统计
        if ((as = counterCells) != null ||
            // 1.1 第一次竞争 竞争修改baseCount
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            // 1.2 baseCount竞争失败,a.CountCells未初始化时,进入fullAddCount进行初始化
            // b. CountCells[]已经初始化,且索引处没有值时,进入fullAddCount进行添加
            // c. CountCells处有值时,竞争修改值,竞争失败,进入fullAddCount中
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            // 1.3 baseCount竞争失败,就会统计元素个数
            s = sumCount();
        }
        // 2.多线程并发扩容table,竞争非常激烈的时候,s大于了阈值
        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) << RESIZE_STAMP_SHIFT;
                if (sc < 0) {
                    if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                        (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 + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

分析: 1.该统计元素个数,采用的是baseCount计数和CountCells列表计数,保证在并发场景下计数的效率提升。但是该sumCount 没有加锁,不能保证实时一致性,只能保证最终一致性,因为baseCount和CountCells都是CAS操作。

当竞争非常激烈的时候

第一次竞争,竞争baseCount U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)竞争成功,直接在baseCount上加,baseCount竞争失败,则进入第二次竞争

第二次竞争,此时countCells未初始化,进入fullAddCount中,初始化一个长度为2的数组,并把元素个数放入数组中

java 复制代码
    private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
        // 1. 获得线程安全随机数
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        // 2.自旋,保证统计个数的成功
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            // 3.初始化CountCells CAS保证初始化成功
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                boolean init = false;
                try {  
                    // 初始化为一个长度为2的数组,并把value 元素个数 放入数组中
                    if (counterCells == as) {
                        CounterCell[] rs = new CounterCell[2];
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
        }
    }

第三次竞争:竞争baseCount失败,并且此时CountCells数组索引处没值的话,则fullAddCount进行添加:

java 复制代码
            if ((as = counterCells) != null && (n = as.length) > 0) {
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {            // Try to attach new Cell
                        CounterCell r = new CounterCell(x); // Optimistic create
                        //1. CAS 竞争添加 假如一个线程添加成功,另一个线程竞争失败
                        if (cellsBusy == 0 &&
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            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;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                // 2.若竞争空值失败 则直接在有值的地方进行添加,若这里仍有其它线程竞争,再次竞争失败
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break; 
                else if (counterCells != as || n >= NCPU)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                // 3. 索引处有值竞争失败,则继续竞争进行CAS操作进行 扩容countCells
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        if (counterCells == as) {// Expand table unless stale
                           // 4.扩容为原来的两倍
                            CounterCell[] rs = new CounterCell[n << 1];
                           // 5.遍历原数组,直接放到新数组里面
                            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);
// 6.如果上面都竞争失败了,说明CountCells数组竞争已经相当激烈了,该线程回去竞争baseCount去
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
            }

第四次竞争: 在索引处有值的时候。如果竞争失败,则进行CountCells数组的扩容,扩容为CAS操作,如果此时也竞争失败,表明CountCells的竞争已经相当激烈了,再回去竞争baseCount去。

元素个数统计整体流程图如下:

5. ConcurrentHashMap总结

  1. 存在线程竞争的table初始化、赋值以及扩容、数据转移处采用细粒度的CAS操作提升并发效率,整体主要是CAS机制和同步代码块结合使用

  2. 多线程并发协助扩容和数据转移,在数据转移处,采用多线程并发协助转移各个不同的分段区间,来提升table扩容时数据迁移的效率

3.数据迁移时的链表采用高低位快速迁移的方法提升效率

4.元素个数的统计为了应对高并发场景下,单个变量的统计会进行阻塞,则采用baseCount与CountCells数组列表相结合的方式,使得高并发下的统计的元素个数有多个存储的地方,提升效率。

相关推荐
myNameGL23 分钟前
linux安装idea
java·ide·intellij-idea
青春男大25 分钟前
java栈--数据结构
java·开发语言·数据结构·学习·eclipse
HaiFan.1 小时前
SpringBoot 事务
java·数据库·spring boot·sql·mysql
我要学编程(ಥ_ಥ)1 小时前
一文详解“二叉树中的深搜“在算法中的应用
java·数据结构·算法·leetcode·深度优先
music0ant1 小时前
Idea 添加tomcat 并发布到tomcat
java·tomcat·intellij-idea
计算机徐师兄2 小时前
Java基于SSM框架的无中介租房系统小程序【附源码、文档】
java·微信小程序·小程序·无中介租房系统小程序·java无中介租房系统小程序·无中介租房微信小程序
源码哥_博纳软云2 小时前
JAVA智慧养老养老护理帮忙代办陪诊陪护小程序APP源码
java·开发语言·微信小程序·小程序·微信公众平台
忒可君3 小时前
C# winform 报错:类型“System.Int32”的对象无法转换为类型“System.Int16”。
java·开发语言
斌斌_____3 小时前
Spring Boot 配置文件的加载顺序
java·spring boot·后端
路在脚下@3 小时前
Spring如何处理循环依赖
java·后端·spring