Skip to content

Commit a4b10f2

Browse files
author
REALROOK1E
committed
补充ConcurrentHashMap size()方法的分段计数机制详解
1 parent 28cb0b8 commit a4b10f2

File tree

1 file changed

+47
-0
lines changed

1 file changed

+47
-0
lines changed

docs/java/collection/concurrent-hash-map-source-code.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,53 @@ public V get(Object key) {
597597
3. 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。
598598
4. 如果是链表,遍历查找之。
599599

600+
### 5. size 计数
601+
602+
`ConcurrentHashMap``size()` 方法用来获取当前 Map 中元素的总数,但在高并发场景下,如何准确且高效地统计元素数量是一个技术难点。Java8 采用了一套精巧的分段计数机制来解决这个问题。
603+
604+
#### 5.1 为什么需要分段计数
605+
606+
在并发环境下,如果多个线程同时执行 `put` 操作,它们都需要更新元素总数。如果使用一个共享的计数器变量,就会导致激烈的竞争——所有线程都在争抢同一个变量的修改权,这会严重影响性能。
607+
608+
为了解决这个问题,`ConcurrentHashMap` 采用了**分散热点**的设计思想:不使用单一计数器,而是将计数分散到多个变量中。就像银行不会只开一个窗口办业务,而是开多个窗口分流客户一样,这样可以大大减少冲突。
609+
610+
#### 5.2 baseCount 和 counterCells 的设计
611+
612+
`ConcurrentHashMap` 内部维护了两个关键的计数相关字段:
613+
614+
- **baseCount**:基础计数器,在没有竞争的情况下,直接通过 CAS 更新这个变量。可以把它理解为"主计数器"。
615+
- **counterCells**:计数器数组,当多个线程竞争 `baseCount` 失败时,会尝试将计数增量分散到 `counterCells` 数组的不同位置。每个线程根据其线程 ID 映射到数组的某个位置,在自己的"专属格子"里进行计数累加,从而避免竞争。
616+
617+
**举个例子**:假设有 10 个线程同时往 Map 中添加元素。第一个线程成功通过 CAS 更新了 `baseCount`,但后面 9 个线程在更新 `baseCount` 时发现有竞争,就会转而去 `counterCells` 数组中找一个位置进行累加。这 9 个线程可能分散到数组的不同位置,比如线程 2 在 `counterCells[1]` 累加,线程 3 在 `counterCells[2]` 累加,以此类推。这样就把竞争从一个点分散到了多个点,大大降低了冲突概率。
618+
619+
#### 5.3 put 元素时如何更新计数
620+
621+
`putVal` 方法的最后,我们可以看到调用了 `addCount(1L, binCount)` 方法,这个方法就是用来更新元素计数的。
622+
623+
`addCount` 的执行逻辑如下:
624+
625+
1. **优先尝试更新 baseCount**:首先尝试通过 CAS 操作直接更新 `baseCount`,如果成功就结束。这是最理想的情况,没有竞争,性能最高。
626+
627+
2. **竞争时使用 counterCells**:如果 CAS 更新 `baseCount` 失败(说明有其他线程在竞争),则会尝试在 `counterCells` 数组中找到一个属于当前线程的位置,然后对该位置的计数值进行 CAS 累加。
628+
629+
3. **动态扩容 counterCells**:如果 `counterCells` 数组还未初始化,或者数组中的某个位置依然存在激烈竞争,`addCount` 方法会动态地扩容 `counterCells` 数组,增加更多的计数槽位,进一步分散竞争。
630+
631+
这种设计保证了在低并发时使用简单的 `baseCount`,在高并发时自动切换到分段计数,兼顾了性能和准确性。
632+
633+
#### 5.4 sumCount 如何计算元素总数
634+
635+
当我们调用 `size()` 方法时,最终会调用 `sumCount()` 方法来计算元素总数。`sumCount()` 的逻辑非常简单直接:
636+
637+
1. 先读取 `baseCount` 的值作为基础值
638+
2. 遍历整个 `counterCells` 数组,将每个位置的计数值累加到基础值上
639+
3. 返回最终的累加结果
640+
641+
需要注意的是,`sumCount()` 并不会加锁,所以返回的结果是一个**近似值**。在调用 `size()` 的瞬间,可能有其他线程正在修改计数,因此得到的不一定是完全精确的实时值。但这在实际应用中通常是可以接受的,因为在高并发场景下,"此时此刻的准确元素个数"本身就是一个动态变化的概念。
642+
643+
**举个例子**:假设当前 `baseCount = 100``counterCells` 数组有 4 个元素,分别是 `[5, 8, 3, 6]`,那么 `sumCount()` 返回的结果就是 `100 + 5 + 8 + 3 + 6 = 122`。这个计算过程中不需要加锁,速度很快,即使在计算过程中有新元素插入,影响也很小。
644+
645+
通过这种"无锁读取 + 分段累加"的方式,`size()` 方法在保证性能的同时,也能给出一个合理的元素总数估计值。
646+
600647
总结:
601648

602649
总的来说 `ConcurrentHashMap` 在 Java8 中相对于 Java7 来说变化还是挺大的,

0 commit comments

Comments
 (0)