news 2026/5/26 6:15:30

ConcurrentHashMap size () 流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ConcurrentHashMap size () 流程

要彻底理解ConcurrentHashMapsize()流程,核心是抓住不同JDK版本的设计差异(JDK7基于分段锁,JDK8基于无锁计数),以及「并发下计数准确性」与「性能」的平衡思路。以下是分版本的详细拆解:

一、核心背景:为什么size()不能简单累加?

ConcurrentHashMap的核心优势是无全局锁,支持高并发读写。如果直接加全局锁统计元素个数,会丧失并发优势;如果不加锁直接累加,又会因并发修改导致计数不准。因此size()的设计目标是:在尽可能少的锁开销下,获取足够准确的元素个数(注意:返回的是「近似值」,非绝对精确)。

二、JDK7版本:基于Segment分段锁的size()流程

1. JDK7 ConcurrentHashMap 结构基础

JDK7的核心是「分段锁(Segment)」:

  • 整个容器分为多个Segment(默认16个),每个Segment是一个独立的哈希表,继承ReentrantLock,自身维护一个count变量(记录该Segment内的元素个数);
  • 并发读写时,仅锁定单个Segment,不同Segment的操作互不阻塞。
2. size() 具体执行流程

size()的本质是累加所有Segment的count值,流程分「轻量级尝试」和「重量级加锁」两步:

步骤操作逻辑目的
1. 轻量级尝试(最多3次)① 遍历所有Segment,不加锁,逐个读取Segment.count并累加,得到总和sum1
② 再次遍历累加,得到sum2
③ 第三次遍历累加,得到sum3
避免一上来加全局锁,利用“短时间内无并发修改”的概率,提升性能
2. 判断尝试结果如果sum1 == sum2 == sum3:说明3次累加过程中无并发修改,计数准确,直接返回该值;
如果不一致:说明有线程在并发添加/删除元素,进入「重量级加锁」;
3次是经验值,平衡“重试开销”和“锁开销”
3. 重量级加锁① 遍历所有Segment,调用Segment.lock()加锁(全局锁,所有Segment被锁定);
② 重新遍历所有Segment,累加count值;
③ 遍历完成后,调用Segment.unlock()解锁;
④ 返回最终累加结果;
保证计数绝对准确,但牺牲了并发性能(仅在轻量级尝试失败时触发)
3. JDK7 size() 关键细节
  • countvolatile变量:每个Segment的countvolatile修饰,保证单个Segment的计数可见性;
  • 为什么最多试3次?:如果重试次数太少,容易误触发全局锁;次数太多,重试本身的开销会超过锁开销,3次是JDK团队的经验最优值;
  • 性能特点:低并发下(无修改),仅遍历3次,无锁开销;高并发下(有修改),触发全局锁,性能下降。

三、JDK8版本:基于无锁计数的size()流程(推荐重点掌握)

1. JDK8 ConcurrentHashMap 结构基础

JDK8彻底取消了Segment,核心结构改为「数组+链表+红黑树」,并发控制依赖CAS + synchronized(桶级锁);计数逻辑借鉴了LongAdder的思想(分段计数),核心是baseCount + CounterCell[]

2. 计数核心组件
组件作用特性
baseCount(volatile long)基础计数值无并发竞争时,直接CAS更新该值,效率最高
CounterCell[](数组)分段计数值当CAS更新baseCount失败(有竞争),将增量哈希到某个CounterCell,更新该Cell的值(避免所有线程竞争同一个变量)
modCount(volatile int)修改计数器记录容器的修改次数(添加/删除元素时+1),用于判断size()累加过程中是否有并发修改
3. size() 具体执行流程

JDK8的size()mappingCount()的简化版(size()返回int,可能溢出;mappingCount()返回long,推荐使用),流程如下:

graph TD A[初始化总和sum=0] --> B[累加baseCount到sum] B --> C{CounterCell数组是否非空?} C -- 是 --> D[遍历CounterCell数组,累加每个Cell的value到sum] C -- 否 --> E[检查并发修改] D --> E[检查modCount是否变化(是否有并发修改)] E -- 有修改 --> A[重试累加(自旋)] E -- 无修改 --> F[返回sum]

分步拆解

  1. 基础累加:先将baseCount(基础计数)的值累加到总和sum
  2. 分段累加:如果CounterCell[]非空(说明有并发竞争),遍历数组,将每个CounterCellvalue(分段计数)累加到sum
  3. 并发校验:检查累加过程中modCount是否变化(若变化,说明有线程在添加/删除元素,计数可能不准);
  4. 自旋重试:如果检测到并发修改,重新执行「基础累加+分段累加」,直到无修改或重试次数足够;
  5. 返回结果:返回最终的sum(近似值,累加过程中可能仍有线程修改,但误差可接受)。
4. JDK8 size() 核心设计亮点
  • 无锁计数:全程无全局锁,仅通过CAS和自旋保证计数准确性,符合JDK8“减少锁依赖”的设计思路;
  • 分段计数(CounterCell):借鉴LongAdder,将计数压力分散到多个Cell,避免所有线程竞争同一个baseCount,并发计数性能比JDK7提升一个量级;
  • 近似值妥协:放弃“绝对精确”,换取“无锁高性能”——因为并发场景下,size()的绝对精确无实际意义(返回值的瞬间,元素个数可能已变化)。

四、JDK7 vs JDK8 size() 核心差异对比

维度JDK7JDK8
核心依赖Segment.count + 分段锁baseCount + CounterCell[] + CAS
锁开销低并发无锁,高并发全局锁全程无锁(仅自旋)
计数准确性轻量级尝试失败后,加锁保证绝对准确始终返回近似值(误差可控)
性能高并发下因全局锁性能下降高并发下仍保持高性能(分段计数)
核心思想「锁分段」→ 减少锁范围「无锁分段计数」→ 彻底无锁

五、面试官常追问的关键细节

1. size() 返回的是精确值吗?
  • JDK7:轻量级尝试失败后,加全局锁返回精确值;轻量级尝试成功时,返回“近似精确值”(3次累加一致,大概率无修改);
  • JDK8:始终返回近似值(累加过程中可能有线程修改),但误差极小,满足绝大多数业务场景(如监控、统计)。
2. JDK8为什么用CounterCell而不是Segment?

JDK8取消Segment的核心是「细化锁粒度」(从Segment级→桶级),而CounterCell是「计数维度的分段」,与存储结构解耦:

  • Segment是“存储+锁”的分段,CounterCell仅负责“计数”的分段;
  • CounterCell的数量可动态扩展(按需创建),而Segment数量固定(默认16),更灵活。
3. JDK8为什么推荐用mappingCount()替代size()?

size()返回int类型,最大值为2^31-1(约21亿),当ConcurrentHashMap元素超过该值时会溢出;mappingCount()返回long类型,无溢出风险,语义与size()完全一致。

4. 计数时如何保证线程安全?
  • baseCountCounterCell.value都是volatile修饰,保证可见性;
  • 更新baseCount时用CAS(Unsafe.compareAndSwapLong),更新CounterCell时也用CAS,避免线程冲突;
  • 遍历CounterCell时,通过Unsafe直接访问内存,保证遍历过程的原子性。

六、总结

ConcurrentHashMapsize()流程,本质是并发场景下“计数准确性”与“性能”的权衡

  • JDK7:先无锁重试,失败后加全局锁,兼顾“低并发性能”和“高并发准确性”;
  • JDK8:彻底无锁,基于分段计数(CounterCell)返回近似值,极致优化高并发性能;
  • 核心思想:并发容器的计数无需绝对精确,优先保证读写性能,这也是ConcurrentHashMap与Hashtable(全表锁计数)的核心区别。

------------------------------------------------------------------------------------------------------

要彻底讲清ConcurrentHashMapsize()执行过程,我们分JDK7(分段锁)JDK8(无锁分段计数)两个版本展开,且聚焦「代码级的具体执行步骤 + 底层原理」,而非仅框架性描述。

前置说明

size()的核心目标是在并发读写场景下,以最小的性能开销获取元素个数(JDK7 兼顾“精确性”,JDK8 优先“高性能”,放弃绝对精确)。
先明确两个版本的核心计数载体:

版本核心计数变量并发控制方式
JDK7每个Segment内置volatile int count(记录该分段的元素数)分段锁(ReentrantLock
JDK8baseCount(基础计数,volatile long) +CounterCell[](分段计数数组,每个元素是volatile long valueCAS + 自旋 + 无锁

一、JDK7 版本:基于 Segment 分段锁的 size() 详细流程

1. JDK7 底层结构回顾

ConcurrentHashMap被拆分为16个默认Segment(可通过构造参数调整),每个 Segment 是独立的哈希表,且继承ReentrantLock(分段锁)。

  • 每个 Segment 有volatile int count:记录该分段内的元素个数,volatile保证单个分段计数的可见性;
  • 新增/删除元素时,仅锁定目标 Segment,修改其count
  • size()需累加所有 Segment 的count

2. size() 完整执行步骤(附伪代码)

publicintsize(){// 步骤1:定义核心变量finalSegment<K,V>[]segments=this.segments;intsum;// 最终累加结果intretries=-1;// 重试次数(初始-1,标记首次尝试)// 步骤2:轻量级无锁尝试(最多3次)for(;;){if(retries++==3){// 重试3次失败,触发重量级加锁// 步骤3:全局加锁(锁定所有Segment)for(inti=0;i<segments.length;++i){segments[i].lock();// 逐个锁定Segment(全局锁)}break;// 跳出循环,准备累加}sum=0;// 每次尝试重置总和intcheck=0;// 校验值,记录修改次数(防并发修改)// 步骤4:遍历所有Segment,无锁累加countfor(inti=0;i<segments.length;++i){Segment<K,V>seg=segments[i];if(seg!=null){sum+=seg.count;// 读取volatile count,累加check+=seg.modCount;// modCount:该Segment的修改次数(增/删元素+1)}}// 步骤5:校验是否有并发修改(check=0 表示所有Segment都无修改)if(check==0){break;// 无修改,计数准确,跳出循环}// 有修改 → 重试(进入下一轮for循环)}// 步骤6:若加了全局锁,需解锁if(retries==3){for(inti=0;i<segments.length;++i){segments[i].unlock();// 逐个解锁Segment}}// 步骤7:返回结果(int可能溢出,JDK7后期新增了long size64())returnsum;}

3. 每一步的细节拆解

步骤2:轻量级无锁尝试(最多3次)
  • 为什么是3次?JDK团队的经验值:3次重试的开销 < 全局加锁的开销,且3次内大概率能遇到“无并发修改”的窗口期;
  • 每次尝试都会重新遍历所有Segment,读取最新的count(因为count是volatile,读取的是内存最新值)。
步骤4:累加 count + 校验 modCount
  • modCount是每个 Segment 的修改计数器(增/删元素时原子+1),check累加所有 Segment 的modCount
    • check=0:所有 Segment 自本次尝试开始后无任何修改,sum是准确值;
    • check≠0:有 Segment 被并发修改,sum可能不准,触发重试。
步骤3:全局加锁(重试3次失败后)
  • 锁定所有 Segment:此时所有增/删操作都会被阻塞(因为增/删需要先锁对应 Segment);
  • 加锁后重新累加count:此时无并发修改,sum绝对精确值
步骤6:解锁
  • 必须保证“加锁的Segment全部解锁”,否则会导致后续所有操作阻塞(死锁风险)。

4. JDK7 size() 关键特性

  • 低并发场景:3次内大概率check=0,无锁开销,性能极高;
  • 高并发场景:重试3次失败后加全局锁,性能骤降,但保证计数精确;
  • 结果:返回值要么是“无修改的精确值”,要么是“加锁后的精确值”。

二、JDK8 版本:基于无锁分段计数的 size() 详细流程(重点)

1. JDK8 底层结构回顾

  • 取消 Segment,核心结构为「数组+链表+红黑树」,并发控制依赖CAS + synchronized(桶级锁);
  • 计数逻辑借鉴LongAdder:将计数分散到baseCountCounterCell[],避免所有线程竞争同一个变量。

2. 核心计数变量(先理解“写计数”,再理解“读计数”)

size()是“读计数”,需先知道增/删元素时“写计数”的逻辑,才能理解读取流程:

变量类型作用写计数逻辑(增/删元素时)
baseCountvolatile long基础计数无并发竞争时,直接 CAS 更新baseCount(+1/-1);CAS 失败则走CounterCell
counterCellstransient volatile CounterCell[]分段计数数组1. 计算当前线程的哈希值(ThreadLocalRandom.getProbe()),定位到数组中的一个CounterCell
2. CAS 更新该CounterCellvalue(+1/-1);
3. 若数组未初始化/目标 Cell 为 null,初始化数组并创建 Cell;
4. 若 CAS 仍失败,重新计算哈希值,重试(避免哈希冲突)
modCountvolatile int修改计数器每次增/删元素时modCount++,用于校验读取过程中是否有并发修改
CounterCell静态内部类分段计数单元仅含一个volatile long value字段,保证计数可见性

3. size() 完整执行步骤(附伪代码 + 逐行解释)

JDK8 中size()mappingCount()的简化版(size()返回 int,可能溢出;mappingCount()返回 long,推荐使用),核心逻辑一致:

publiclongmappingCount(){// 步骤1:初始化变量longsum=0L;// 最终累加结果longcheck=0L;// 校验用的modCount快照intretries=-1;// 重试次数// 步骤2:获取计数核心组件(通过Unsafe直接访问底层变量)CounterCell[]cs=counterCells;// 步骤3:自旋累加(直到无并发修改或重试足够)for(;;){if(retries++==1){// 重试1次失败,放弃modCount校验(避免无限自旋)break;}sum=0L;// 重置总和check=modCount;// 记录当前modCount(快照)// 步骤4:累加baseCount(基础计数)sum+=baseCount;// 步骤5:累加CounterCell数组(分段计数)if(cs!=null){for(CounterCellc:cs){// 遍历所有非空Cellif(c!=null){sum+=c.value;// 读取volatile value,累加}}}// 步骤6:校验是否有并发修改(modCount未变 → 无修改)if(modCount==check){break;// 无修改,跳出循环}// 有修改 → 自旋重试(进入下一轮for循环)}// 步骤7:返回累加结果(近似值)returnsum;}

4. 每一步的细节拆解

步骤1:变量初始化
  • sum:最终返回的元素个数,初始为0;
  • check:记录读取开始时的modCount,用于后续校验;
  • retries:重试次数初始为-1,首次循环retries++变为0,重试1次后变为1,触发跳出。
步骤3:自旋累加(核心)
  • 自旋的目的:尽可能获取“无并发修改”的计数结果;
  • 仅重试1次:JDK8 认为“近似值足够”,过多重试会浪费性能(即使返回近似值,误差也极小)。
步骤4:累加 baseCount
  • baseCountvolatile类型,读取时直接获取内存最新值,无锁开销;
  • 若并发度低(无线程竞争),counterCells为 null,sum就是baseCount,直接返回。
步骤5:累加 CounterCell 数组
  • 遍历counterCells时,仅读取非空 Cell 的valuevalue是 volatile,保证最新);
  • 遍历过程中可能有线程修改 Cell 的value,但因为是“读取”,无需加锁,仅会导致 sum 是“瞬时近似值”。
步骤6:校验 modCount
  • modCountvolatile类型,读取的是最新值;
  • modCount == check:说明从“记录check”到“累加完成”的过程中,无线程增/删元素,sum 是“准精确值”;
  • modCount != check:说明有并发修改,自旋重试1次;若重试后仍有修改,直接返回当前 sum(放弃精确性)。
步骤7:返回结果
  • 结果是近似值:即使modCount未变,累加过程中也可能有线程修改baseCount/CounterCell(因为读取是“非原子的遍历”),但误差在高并发下可忽略;
  • 为什么放弃绝对精确?:并发场景下,size()返回值的瞬间,元素个数可能已变化,绝对精确无业务意义,不如优先保证性能。

5. JDK8 size() 关键细节补充

(1)CounterCell 的哈希定位逻辑

线程更新CounterCell时,通过ThreadLocalRandom.getProbe()获取当前线程的“探针值”(哈希值),再通过probe & (cs.length - 1)定位到数组下标(类似哈希表的取模),避免所有线程竞争同一个 Cell。

(2)Unsafe 工具类的作用

baseCountcounterCellsmodCount都是通过Unsafe类直接操作内存地址更新/读取,绕过 JVM 层面的锁,保证操作的原子性和性能。

(3)为什么 JDK8 不考虑加锁?

JDK8 的设计理念是“并发容器的计数无需绝对精确”:

  • 业务场景中,size()多用于“监控/统计/限流阈值判断”,近似值足够;
  • 加锁会阻塞所有增/删操作,违背ConcurrentHashMap“高并发”的核心设计目标。

三、两个版本 size() 过程的核心差异总结

维度JDK7 执行过程JDK8 执行过程
核心动作无锁重试3次 → 失败则全局加锁 → 精确累加无锁自旋1次 → 累加 baseCount + CounterCell → 近似返回
锁开销低并发无锁,高并发全局锁(性能骤降)全程无锁(仅自旋,性能无损耗)
结果特性绝对精确(加锁后)/ 准精确(无锁重试成功)始终近似(误差可控)
底层依赖Segment 的 count(volatile) + ReentrantLockbaseCount + CounterCell(volatile) + CAS + Unsafe
性能优先级精确性 > 性能性能 > 精确性

四、面试官常追问的“细节中的细节”

1. JDK7 中如果 Segment 被扩容,size() 会出错吗?

不会。Segment 扩容是“分段扩容”(仅扩容当前 Segment),扩容过程中该 Segment 的count仍会正确更新(扩容时先加锁,再迁移元素,最后更新 count),size() 读取的是扩容后的 count。

2. JDK8 中 CounterCell 数组是怎么初始化的?

首次 CAS 更新baseCount失败时,会通过fullAddCount()方法初始化counterCells

  • 先尝试 CAS 创建长度为2的数组;
  • 若仍有竞争,数组会按2倍扩容(类似 HashMap 扩容),避免哈希冲突。

3. size() 返回的“近似值”误差有多大?

极端场景下(百万级并发增/删),误差可能在“个位数到百位数”之间;但常规业务场景(万级并发),误差几乎可以忽略(通常≤10)。

4. 如何在 JDK8 中获取绝对精确的元素个数?

若业务必须精确,可手动加全局锁(不推荐):

// 不推荐:会阻塞所有并发操作,丧失ConcurrentHashMap的优势synchronized(concurrentHashMap){longexactSize=concurrentHashMap.mappingCount();}

替代方案:业务层面通过“分布式计数器”(如 Redis)记录元素个数,而非依赖size()

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/22 23:49:08

IndraDB:Rust语言构建的高性能图数据库

IndraDB&#xff1a;Rust语言构建的高性能图数据库 【免费下载链接】indradb A graph database written in rust 项目地址: https://gitcode.com/gh_mirrors/in/indradb 在当今数据驱动的时代&#xff0c;图数据库正成为处理复杂关系数据的首选工具。IndraDB作为一款用R…

作者头像 李华
网站建设 2026/5/26 5:34:32

Abracadabra魔曰:终极汉字加密工具完全指南

在现代数字安全领域&#xff0c;一款名为Abracadabra(魔曰)的开源加密工具正在重新定义文本保护的概念。这款工具巧妙地将传统文言文美学与现代加密技术相结合&#xff0c;为用户提供前所未有的安全体验。 【免费下载链接】Abracadabra Abracadabra 魔曰&#xff0c;下一代文本…

作者头像 李华
网站建设 2026/5/26 5:03:24

基于Dlib的疲劳驾驶检测系统:从零开始的终极安装使用指南

基于Dlib的疲劳驾驶检测系统&#xff1a;从零开始的终极安装使用指南 【免费下载链接】Fatigue-Driving-Detection-Based-on-Dlib 项目地址: https://gitcode.com/gh_mirrors/fa/Fatigue-Driving-Detection-Based-on-Dlib 想要实现精准的疲劳驾驶实时检测吗&#xff1f…

作者头像 李华
网站建设 2026/5/26 5:35:46

工业DCS数据通信物联网解决方案

某企业从事涂剂材料的生产制造工作&#xff0c;生产规模逐渐扩大&#xff0c;工厂新增了一套DCS系统&#xff0c;要求将原先的DCS接入到新增DCS系统部署的平台中&#xff0c;实现集中管控并提升全厂自动化水平。对于原有的DCS&#xff0c;需要加装通信网关实现OPC UA转Modbus的…

作者头像 李华
网站建设 2026/5/26 5:33:54

创客匠人2025全球峰会赋能“AI+IP”融合增长

2025年11月&#xff0c;厦门海峡大剧院&#xff0c;由创客匠人主办的“全球创始人IPAI万人高峰论坛”盛大启幕。这场汇聚全球万名创始人与行业领袖的思想盛会&#xff0c;不仅见证了AI技术与个人品牌价值的深度碰撞&#xff0c;更昭示着一个全新商业时代的到来。生态赋能&#…

作者头像 李华