2026/6/28 5:31:31
网站建设
项目流程
东莞网站建设哪家公司好,别人做的网站打不开,海南网上办事大厅官网,北京市建设信息网站面试官问题结构化回答#xff1a;ConcurrentHashMap原理、扩容及扩容时的线程安全
核心总览
ConcurrentHashMap#xff08;CHM#xff09;是JUC包下为解决「HashMap线程不安全、Hashtable全表锁效率低」设计的并发安全哈希表#xff0c;核心目标是「高并发下的线程安全 尽…面试官问题结构化回答ConcurrentHashMap原理、扩容及扩容时的线程安全核心总览ConcurrentHashMapCHM是JUC包下为解决「HashMap线程不安全、Hashtable全表锁效率低」设计的并发安全哈希表核心目标是「高并发下的线程安全 尽可能少的锁竞争」。其核心演进分为两个版本JDK1.7基于「分段锁Segment」实现锁粒度为「Segment数组的一个元素」不同Segment的操作可并发JDK1.8抛弃分段锁采用「Node数组 CAS 局部synchronized桶级锁 红黑树」锁粒度细化到「单个哈希桶」并通过CAS减少锁使用性能远超1.7扩容是CHM的核心难点1.8的扩容设计为「多线程协助扩容」避免单线程扩容阻塞线程安全依赖「扩容标识sizeCtl、CAS、synchronized、ForwardingNode占位」四层保障。一、ConcurrentHashMap核心原理1.7 vs 1.81. JDK1.7 核心设计分段锁Segment HashEntry结构层级作用线程安全保障Segment数组ReentrantLock子类核心锁载体CHM的外层数组默认16个Segment每个Segment独立加锁不同Segment的操作可并发如操作Segment[0]和Segment[1]无锁竞争HashEntry数组每个Segment内存储实际键值对结构同HashMap的Entrykey/value/next/hash仅在操作同一Segment内的HashEntry时需获取该Segment的锁核心逻辑通过「两次哈希」定位元素——先哈希到Segment再哈希到HashEntry优点锁粒度比Hashtable全表锁细并发度Segment数量默认16缺点Segment数量固定无法动态扩容极端情况下所有key哈希到同一Segment退化为单锁性能下降。2. JDK1.8 核心设计Node数组 CAS synchronized 红黑树彻底抛弃Segment直接复用HashMap的「数组链表红黑树」结构线程安全通过以下手段实现核心手段作用场景CAS无锁操作初始化桶、插入第一个节点、更新size计数等无竞争场景避免加锁synchronized桶级锁桶内已有节点时锁定「桶的头节点」仅锁当前桶其他桶可并发操作红黑树桶内链表长度≥8时转为红黑树提升查询效率O(n)→O(logn)Node不可变普通Node的value被final修饰仅能通过替换节点修改值避免并发修改扩容标识sizeCtl标记数组的状态未初始化/正常/扩容中避免多线程重复扩容核心逻辑哈希直接定位到Node数组的桶无分段锁仅当操作「同一桶」时才会加synchronized锁锁粒度极致细化核心改进并发度不再受限理论上桶数量且CAS减少了无竞争场景的锁开销性能比1.7提升数倍。二、ConcurrentHashMap扩容机制1.7 vs 1.8重点1.8扩容的核心目标是「扩大数组容量减少哈希冲突」CHM的扩容触发条件和流程随版本大幅优化1. JDK1.7 扩容逻辑Segment内局部扩容触发条件单个Segment内的HashEntry数组容量达到「阈值Segment容量×负载因子默认0.75」扩容流程获取当前Segment的锁ReentrantLock阻塞该Segment的所有操作将该Segment内的HashEntry数组扩容为2倍重新哈希所有节点到新数组替换旧数组释放锁特点仅扩容单个Segment不影响其他Segment但单Segment扩容时该Segment完全阻塞。2. JDK1.8 扩容逻辑全局数组扩容多线程协助1.8的扩容是「全局扩容」整个Node数组设计为「多线程协助扩容」避免单线程扩容耗时过长核心流程如下1扩容触发条件「新增节点后」putVal时若当前桶的链表长度≥8且数组容量64先扩容数组而非转红黑树「容量达标后」数组元素个数size≥「阈值数组容量×负载因子默认0.75」触发扩容主动触发调用putAll()、resize()等方法时直接触发扩容。2扩容核心流程分3阶段阶段操作细节核心控制sizeCtl准备阶段1. 检查sizeCtl若为负数说明已有线程在扩容当前线程协助扩容2. CAS将sizeCtl从「阈值」改为「-1」标记扩容开始3. 创建新数组容量旧数组×24. 将sizeCtl设置为「-(扩容线程数1)」默认-2标识扩容中sizeCtl含义- 正数下次扩容阈值- 0初始状态- -1正在初始化数组- 负数≠-1-(扩容线程数1)如-2表示1个线程在扩容迁移阶段1. 遍历旧数组的每个桶从后往前分配给不同线程迁移2. 锁定当前桶synchronized避免迁移时并发修改3. 将桶内节点重新哈希到新数组高位哈希判断归属4. 迁移完成后在旧桶中放入「ForwardingNode转发节点」标记该桶已迁移ForwardingNode的作用引导后续读写操作直接访问新数组避免操作旧桶完成阶段1. 所有桶迁移完成后将新数组替换旧数组2. CAS将sizeCtl设置为「新数组容量×0.75」新的扩容阈值3. 扩容结束恢复正常状态sizeCtl从负数恢复为正数新阈值3多线程协助扩容的逻辑每个线程负责迁移「一段连续的桶」如线程1迁移015号桶线程2迁移1631号桶若线程A迁移到某桶时发现该桶已被线程B迁移有ForwardingNode则跳过继续迁移下一个桶迁移过程中新的读写请求会「先协助扩容再执行自身逻辑」如put时发现桶是ForwardingNode先迁移一个桶再插入数据。三、扩容时的线程安全保障核心重点CHM扩容时的线程安全是面试高频考点1.7和1.8的保障手段差异显著重点讲1.81. JDK1.7 扩容的线程安全分段锁独占扩容前先获取Segment的ReentrantLock锁该Segment的所有读写操作put/get/remove都会被阻塞直到扩容完成释放锁优点简单粗暴完全避免并发修改缺点单Segment扩容时该Segment的操作全部阻塞并发度低。2. JDK1.8 扩容的线程安全四层保障核心1.8通过「无锁锁占位协作」四层机制既保证线程安全又不影响并发性能1扩容标识sizeCtl避免重复扩容sizeCtl是volatile变量保证多线程可见性扩容前先CAS修改sizeCtl为负数标记扩容中其他线程看到负数后不会重复触发扩容而是协助扩容扩容过程中sizeCtl的数值如-2、-3标识当前扩容线程数避免线程冲突。2CAS无竞争场景的原子性初始化数组、修改sizeCtl、插入第一个节点等操作通过CAS保证原子性如CAS修改sizeCtl为-1避免多线程同时初始化扩容无锁操作减少了锁竞争提升扩容效率。3synchronized桶级别的排他锁迁移某桶时先锁定该桶的头节点synchronized (f)f为桶的头节点确保同一时间只有一个线程迁移该桶锁粒度仅为「单个桶」其他桶的迁移/读写操作可正常并发不会阻塞。4ForwardingNode迁移完成的占位与引导某桶迁移完成后旧桶中放入ForwardingNode特殊节点hash值为-1标记该桶已迁移后续读写操作遇到ForwardingNode时会执行两个逻辑读操作直接到新数组中查询避免读取旧桶的无效数据写操作put/remove先协助扩容迁移一个未处理的桶再执行自身逻辑加速扩容完成ForwardingNode避免了「旧桶数据被修改」和「读写操作访问无效数据」的问题。5节点操作的原子性普通Node的value被final修饰仅能通过「替换节点」如CAS替换头节点、synchronized替换链表节点修改值避免扩容时并发修改节点内容迁移节点时通过「高位哈希hash newCap」判断节点归属新数组的哪个桶保证节点迁移的正确性。四、总结面试收尾金句CHM的核心演进1.7分段锁Segment→1.8 CAS桶级synchronized锁粒度更细并发性能更高1.8扩容的核心设计「多线程协助扩容」替代单线程扩容通过sizeCtl控状态、ForwardingNode引导读写、synchronized锁桶兼顾效率与安全扩容时的线程安全核心1.7靠Segment独占锁1.8靠「sizeCtl标识CAS桶级synchronizedForwardingNode」既避免重复扩容又保证迁移过程中数据不被并发修改。面试追问应对问“CHM 1.8扩容时get操作能正常执行吗”答可以。get操作是无锁的若桶未迁移直接读旧桶若桶已迁移有ForwardingNode则到新数组读若桶正在迁移被synchronized锁定get会自旋等待锁释放不会阻塞保证读操作的高并发。问“CHM 1.8为什么抛弃分段锁”答分段锁的并发度受限于Segment数量默认16且Segment是重量级锁ReentrantLock1.8的桶级synchronized轻量级锁偏向锁/自旋锁优化CAS锁粒度更细并发度理论上无上限且无锁操作更多性能更高。问“CHM扩容时put操作会阻塞吗”答不会完全阻塞。put操作遇到未迁移的桶会锁定该桶执行插入遇到ForwardingNode会先协助扩容迁移一个桶再执行插入仅当操作正在迁移的桶时会短暂等待synchronized锁释放整体仍保持高并发。