@@ -597,6 +597,53 @@ public V get(Object key) {
5975973 . 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。
5985984 . 如果是链表,遍历查找之。
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