Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/java/collection/concurrent-hash-map-source-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,53 @@ public V get(Object key) {
3. 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。
4. 如果是链表,遍历查找之。

### 5. size 计数

`ConcurrentHashMap` 的 `size()` 方法用来获取当前 Map 中元素的总数,但在高并发场景下,如何准确且高效地统计元素数量是一个技术难点。Java8 采用了一套精巧的分段计数机制来解决这个问题。

#### 5.1 为什么需要分段计数

在并发环境下,如果多个线程同时执行 `put` 操作,它们都需要更新元素总数。如果使用一个共享的计数器变量,就会导致激烈的竞争——所有线程都在争抢同一个变量的修改权,这会严重影响性能。

为了解决这个问题,`ConcurrentHashMap` 采用了**分散热点**的设计思想:不使用单一计数器,而是将计数分散到多个变量中。就像银行不会只开一个窗口办业务,而是开多个窗口分流客户一样,这样可以大大减少冲突。

#### 5.2 baseCount 和 counterCells 的设计

`ConcurrentHashMap` 内部维护了两个关键的计数相关字段:

- **baseCount**:基础计数器,在没有竞争的情况下,直接通过 CAS 更新这个变量。可以把它理解为"主计数器"。
- **counterCells**:计数器数组,当多个线程竞争 `baseCount` 失败时,会尝试将计数增量分散到 `counterCells` 数组的不同位置。每个线程根据其线程 ID 映射到数组的某个位置,在自己的"专属格子"里进行计数累加,从而避免竞争。

**举个例子**:假设有 10 个线程同时往 Map 中添加元素。第一个线程成功通过 CAS 更新了 `baseCount`,但后面 9 个线程在更新 `baseCount` 时发现有竞争,就会转而去 `counterCells` 数组中找一个位置进行累加。这 9 个线程可能分散到数组的不同位置,比如线程 2 在 `counterCells[1]` 累加,线程 3 在 `counterCells[2]` 累加,以此类推。这样就把竞争从一个点分散到了多个点,大大降低了冲突概率。

#### 5.3 put 元素时如何更新计数

在 `putVal` 方法的最后,我们可以看到调用了 `addCount(1L, binCount)` 方法,这个方法就是用来更新元素计数的。

`addCount` 的执行逻辑如下:

1. **优先尝试更新 baseCount**:首先尝试通过 CAS 操作直接更新 `baseCount`,如果成功就结束。这是最理想的情况,没有竞争,性能最高。

2. **竞争时使用 counterCells**:如果 CAS 更新 `baseCount` 失败(说明有其他线程在竞争),则会尝试在 `counterCells` 数组中找到一个属于当前线程的位置,然后对该位置的计数值进行 CAS 累加。

3. **动态扩容 counterCells**:如果 `counterCells` 数组还未初始化,或者数组中的某个位置依然存在激烈竞争,`addCount` 方法会动态地扩容 `counterCells` 数组,增加更多的计数槽位,进一步分散竞争。

这种设计保证了在低并发时使用简单的 `baseCount`,在高并发时自动切换到分段计数,兼顾了性能和准确性。

#### 5.4 sumCount 如何计算元素总数

当我们调用 `size()` 方法时,最终会调用 `sumCount()` 方法来计算元素总数。`sumCount()` 的逻辑非常简单直接:

1. 先读取 `baseCount` 的值作为基础值
2. 遍历整个 `counterCells` 数组,将每个位置的计数值累加到基础值上
3. 返回最终的累加结果

需要注意的是,`sumCount()` 并不会加锁,所以返回的结果是一个**近似值**。在调用 `size()` 的瞬间,可能有其他线程正在修改计数,因此得到的不一定是完全精确的实时值。但这在实际应用中通常是可以接受的,因为在高并发场景下,"此时此刻的准确元素个数"本身就是一个动态变化的概念。

**举个例子**:假设当前 `baseCount = 100`,`counterCells` 数组有 4 个元素,分别是 `[5, 8, 3, 6]`,那么 `sumCount()` 返回的结果就是 `100 + 5 + 8 + 3 + 6 = 122`。这个计算过程中不需要加锁,速度很快,即使在计算过程中有新元素插入,影响也很小。

通过这种"无锁读取 + 分段累加"的方式,`size()` 方法在保证性能的同时,也能给出一个合理的元素总数估计值。

总结:

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