concurrent hashmap原理,扩容,扩容时怎么保证线程安全?
2026/5/11 19:18:43 网站建设 项目流程

面试官问题结构化回答:ConcurrentHashMap原理、扩容及扩容时的线程安全

核心总览

ConcurrentHashMap(CHM)是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.8)

1. JDK1.7 核心设计:分段锁(Segment + HashEntry)
结构层级作用线程安全保障
Segment数组(ReentrantLock子类)核心锁载体,CHM的外层数组,默认16个Segment每个Segment独立加锁,不同Segment的操作可并发(如操作Segment[0]和Segment[1]无锁竞争)
HashEntry数组(每个Segment内)存储实际键值对,结构同HashMap的Entry(key/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)」;
  • 扩容流程
    1. 获取当前Segment的锁(ReentrantLock),阻塞该Segment的所有操作;
    2. 将该Segment内的HashEntry数组扩容为2倍,重新哈希所有节点到新数组;
    3. 替换旧数组,释放锁;
  • 特点:仅扩容单个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. 创建新数组(容量=旧数组×2);
4. 将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.8:

1. JDK1.7 扩容的线程安全:分段锁独占
  • 扩容前先获取Segment的ReentrantLock锁,该Segment的所有读写操作(put/get/remove)都会被阻塞,直到扩容完成释放锁;
  • 优点:简单粗暴,完全避免并发修改;缺点:单Segment扩容时,该Segment的操作全部阻塞,并发度低。
2. JDK1.8 扩容的线程安全:四层保障(核心)

1.8通过「无锁+锁+占位+协作」四层机制,既保证线程安全,又不影响并发性能:

(1)扩容标识(sizeCtl):避免重复扩容
  • sizeCtl是volatile变量,保证多线程可见性;
  • 扩容前先CAS修改sizeCtl为负数(标记扩容中),其他线程看到负数后,不会重复触发扩容,而是协助扩容;
  • 扩容过程中,sizeCtl的数值(如-2、-3)标识当前扩容线程数,避免线程冲突。
(2)CAS:无竞争场景的原子性
  • 初始化数组、修改sizeCtl、插入第一个节点等操作,通过CAS保证原子性(如CAS修改sizeCtl为-1,避免多线程同时初始化扩容);
  • 无锁操作减少了锁竞争,提升扩容效率。
(3)synchronized:桶级别的排他锁
  • 迁移某桶时,先锁定该桶的头节点(synchronized (f),f为桶的头节点),确保同一时间只有一个线程迁移该桶;
  • 锁粒度仅为「单个桶」,其他桶的迁移/读写操作可正常并发,不会阻塞。
(4)ForwardingNode:迁移完成的占位与引导
  • 某桶迁移完成后,旧桶中放入ForwardingNode(特殊节点,hash值为-1),标记该桶已迁移;
  • 后续读写操作遇到ForwardingNode时,会执行两个逻辑:
    • 读操作:直接到新数组中查询,避免读取旧桶的无效数据;
    • 写操作(put/remove):先协助扩容(迁移一个未处理的桶),再执行自身逻辑,加速扩容完成;
  • ForwardingNode避免了「旧桶数据被修改」和「读写操作访问无效数据」的问题。
(5)节点操作的原子性
  • 普通Node的value被final修饰,仅能通过「替换节点」(如CAS替换头节点、synchronized替换链表节点)修改值,避免扩容时并发修改节点内容;
  • 迁移节点时,通过「高位哈希(hash & newCap)」判断节点归属新数组的哪个桶,保证节点迁移的正确性。

四、总结(面试收尾金句)

  1. CHM的核心演进:1.7分段锁(Segment)→1.8 CAS+桶级synchronized,锁粒度更细,并发性能更高;
  2. 1.8扩容的核心设计:「多线程协助扩容」替代单线程扩容,通过sizeCtl控状态、ForwardingNode引导读写、synchronized锁桶,兼顾效率与安全;
  3. 扩容时的线程安全核心:1.7靠Segment独占锁,1.8靠「sizeCtl标识+CAS+桶级synchronized+ForwardingNode」,既避免重复扩容,又保证迁移过程中数据不被并发修改。
面试追问应对
  • 问:“CHM 1.8扩容时,get操作能正常执行吗?”
    答:可以。get操作是无锁的:若桶未迁移,直接读旧桶;若桶已迁移(有ForwardingNode),则到新数组读;若桶正在迁移(被synchronized锁定),get会自旋等待锁释放,不会阻塞(保证读操作的高并发)。
  • 问:“CHM 1.8为什么抛弃分段锁?”
    答:分段锁的并发度受限于Segment数量(默认16),且Segment是重量级锁(ReentrantLock);1.8的桶级synchronized(轻量级锁,偏向锁/自旋锁优化)+CAS,锁粒度更细,并发度理论上无上限,且无锁操作更多,性能更高。
  • 问:“CHM扩容时,put操作会阻塞吗?”
    答:不会完全阻塞。put操作遇到未迁移的桶,会锁定该桶执行插入;遇到ForwardingNode,会先协助扩容(迁移一个桶),再执行插入;仅当操作正在迁移的桶时,会短暂等待synchronized锁释放,整体仍保持高并发。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询