一、基础结构与版本核心差异(高频!必会!)
JDK 1.7 实现:分段锁(Segment)
- 运作原理:
将整个Map拆分为多个Segment(默认16个),每个Segment独立加锁(ReentrantLock)。
写操作时只锁一个Segment,其他Segment仍可并发读写 → 降低锁冲突。 - 类比理解:
好比一栋楼有16个独立房间(Segment),每个房间有自己的锁。A在1号房存东西时,B仍可进2号房拿东西,互不影响。
JDK 1.8 实现:CAS + synchronized 细粒度锁
- 运作原理:
- 取消Segment,结构变为 Node数组 + 链表 + 红黑树(同HashMap)
- 锁粒度细化到单个桶的头节点(链表首节点或红黑树根节点)
- 插入时:
- 桶为空 → CAS无锁插入
- 桶非空 → synchronized锁头节点再插入
- 为何改用synchronized?
JDK1.6后synchronized已优化(锁升级:无锁→偏向锁→轻量锁→重量锁),且节省内存(若用ReentrantLock需每个Node继承AQS,浪费资源)。
二、6大核心机制与面试回答要点
1️⃣ Put流程(线程安全如何保证?)
- 步骤:
① 计算Key的hash值 → 定位桶位置;
② 若桶为空 → CAS插入(失败则重试);
③ 若桶为扩容中转节点(hash=-1) → 协助扩容;
④ 若桶非空 → synchronized锁头节点 → 遍历链表/树插入或更新;
⑤ 链表超8且数组≥64 → 转红黑树。 - 面试话术:“1.8的put通过CAS+桶锁实现安全:空桶无锁竞争用CAS,非空桶锁头节点防并发写。同时支持协助扩容,避免阻塞。”
2️⃣ Get流程(为何无需加锁?)
- 原理:Node的
value
和next
指针用volatile
修饰 → 写操作后其他线程立即可见最新值。 - 注意:与
table
数组是否volatile无关(volatile数组仅保证数组引用可见,不保证元素可见)。 - 面试话术:“get无锁是因为Node内部用volatile保证可见性,读直接访问内存最新值,类似读Redis,无并发冲突。”
3️⃣ 扩容机制(如何高效?)
- 触发条件:元素总数 ≥ 数组长度 × 0.75(负载因子固定0.75)。
- 并发扩容:
- 线程扩容时发现其他线程在扩容 → 协助迁移数据(而非等待);
- 迁移时用ForwardingNode标记已迁桶,写请求遇到该标记会协助扩容。
- 面试话术:“扩容时线程可协同搬运数据,通过ForwardingNode标志让新请求也来帮忙,类似多人搬仓库货架,效率更高。”
4️⃣ Key/Value为何不允许null?
- 二义性问题:
map.get(key)
返回null
时 → 无法区分是“key不存在”还是“value本就是null”;- 多线程下:A线程读
null
时,B线程可能正好写入null
→ 破坏业务逻辑。
- 面试话术:“null值在多线程场景产生二义性,比如缓存系统无法区分‘无数据’和‘数据为null’,因此直接禁止。”
5️⃣ Size计算(如何保证并发准确?)
- 原理:
用LongAdder
分片计数思想:- 维护
baseCount
+CounterCell[]
分片计数器; - 更新时优先CAS改
baseCount
,失败则改分片计数器 → 分散并发压力。
- 维护
- 面试话术:“size不是遍历计数,而是分片累加(baseCount+CounterCell数组),类似超市多个收银台独立计数再汇总,避免所有线程挤一个计数器。”
6️⃣ 迭代器:弱一致性(Weakly Consistent)
- 原理:迭代器创建时不冻结快照,遍历期间可能读到其他线程新增/删除的条目(未遍历部分),但不保证完全实时210。
- 对比:HashMap迭代器是强一致性(遍历中修改直接抛
ConcurrentModificationException
)。 - 面试话术:“迭代器是弱一致的:遍历时可能看到后续修改,但不保证全量最新。类似边看书边有人修改内容——你翻到后面时会看到新内容,但已读过的页不重读。”
三、JDK 1.7 vs 1.8 高频对比表
对比项 | JDK 1.7 | JDK 1.8 | 优化点 |
---|---|---|---|
数据结构 | Segment + HashEntry | Node数组+链表+红黑树 | 结构统一,查询效率提升 |
锁机制 | ReentrantLock(分段锁) | CAS + synchronized(桶级锁) | 锁粒度更细,内存占用更低 |
并发度 | 由Segment数量决定(默认16) | 由数组大小动态决定 | 更灵活,减少竞争 |
扩容 | 仅当前Segment扩容 | 多线程协同扩容 | 迁移效率大幅提升 |
哈希冲突处理 | 链表 | 链表→红黑树(阈值8) | 防链表退化,查询O(1)→O(logN) |
总结1.8优势:锁粒度细(桶级 vs 段级)、内存省(去Segment)、扩容快(协作式)、查询稳(链表转树)。
四、致命细节(易挂点!)
- 树化条件:链表长度 > 8 且 数组长度 ≥ 64(否则优先扩容);
- size()非实时:分片计数有微小延迟,并发极高时可能误差;
- 并发度设置:1.7可设并发度(构造函数参数),1.8已取消。
五、面试表达技巧
- 原理 → 优点 → 对比三段式回答:“比如您问put如何线程安全(原理),1.8用CAS和桶锁实现,优点是冲突时只锁一个桶(优点),不像Hashtable全表锁,也不像1.7锁整个Segment(对比)。”
- 主动关联场景:“像缓存系统——高并发读用volatile无锁get,写用synchronized仅锁单桶,比Collections.synchronizedMap效率高得多。”