前言

普通的 HashMap 在多线程环境下就像一个没有交警的十字路口,随时可能发生数据覆灭。虽然 Hashtable 提供了安全性,但它那“一人用、全班等”的全局锁性能太差。

为了解决高性能与线程安全的矛盾,ConcurrentHashMap 诞生了。从 JDK 1.7 的“分段锁”到 JDK 1.8 的“CAS + synchronized”,它的每一次进化都是对并发极限的探索。


一、 核心架构的跨代演进

1. JDK 1.7:分段锁(Segment)的艺术

在 1.7 版本中,ConcurrentHashMap 像是一个“大集合套小集合”。

  • 结构:它由一个 Segment 数组组成,每个 Segment 内部又是一个类似 HashMap 的结构。

  • 原理:锁是加在 Segment 上的。如果你操作 A 段,我操作 B 段,我们互不影响。

  • 缺点:锁的粒度还是偏大(默认 16 段),且内层结构依然是简单的“数组+链表”。

2. JDK 1.8:粒度极致化(Node + CAS + synchronized)

1.8 版本彻底抛弃了 Segment,直接回归到数组+链表+红黑树的结构。

  • 进化点:锁的粒度不再是“一段”,而是每一个桶(Node)

  • 锁机制:采用了 CAS(无锁化) 处理初始化和空桶插入,用 synchronized 锁定桶的头节点处理碰撞。


二、 核心源码深度解析(JDK 1.8)

1. 为什么不用 ReentrantLock 而用 synchronized?

这是很多大厂面试的高频考点。在 1.8 中回归 synchronized 是基于以下优化:

  • 减少内存开销ReentrantLock 需要额外的 Condition 对象和 WaitQueue 节点,而 synchronized 的锁信息直接存在对象的 Mark Word 中。

  • 偏向锁/轻量级锁优化:JVM 对 synchronized 做了大量底层优化。

  • 节点竞争小:在哈希分布均匀的情况下,同一个桶的竞争其实并不激烈,synchronized 的性能已足够强大。

2. put 方法的并发之美


三、 扩容机制(Transfer):多线程协同搬家

ConcurrentHashMap 的扩容是最体现黑科技的地方。

  • 协同扩容:当一个线程发现正在扩容时,它不会坐等,而是调用 helpTransfer 加入扩容。

  • ForwardingNode:当一个桶位搬完后,会放一个特殊的 ForwardingNode(Hash 值为 -1)。

  • 安全保证:读操作遇到 ForwardingNode 会被转发到新数组,写操作遇到则协助扩容。


四、 size() 计数的并发技巧

HashMap 中,size++ 只是简单的操作。但在高并发下,这会变成性能瓶颈。

ConcurrentHashMap 借鉴了 LongAdder 的思想:

  • 维护一个 baseCount 和一个 CounterCell 数组

  • 线程竞争不激烈时,CAS 更新 baseCount

  • 竞争激烈时,不同线程随机选择 CounterCell 中的一个进行增加。

  • 最后求和:size = baseCount + sum(CounterCells)

总结:通过分散热点,解决了计数器的性能瓶颈。


五、 实战避坑指南

1. ConcurrentHashMap 不是“万能保险”

虽然它的方法是原子的,但复合操作不是。

2. 强一致性 vs 弱一致性

ConcurrentHashMap 的迭代器是弱一致性的。这意味着在遍历过程中,如果其他线程修改了 Map,迭代器不一定会抛出 ConcurrentModificationException,但也不保证能读到最新的修改。这是为了换取极高的遍历性能。


六、 总结:从面试到生产

作为资深开发,你应该掌握的 ConcurrentHashMap 精髓:

  1. 分段治理:无论是 1.7 的 Segment 还是 1.8 的 Node,核心思想都是减小锁的粒度。

  2. 乐观无锁:大量使用 CAS 进行初始化和空桶操作。

  3. 协同合作:扩容不再是一个人的战斗,而是所有线程“多点协作”。

  4. 动态转换:链表与红黑树的切换保证了极端情况下的查询底线。


结语:通过这几篇长文,我们从最简单的数组聊到了最复杂的并发容器。源码不是为了背诵,而是为了在面对复杂的系统架构时,你能拥有那种“洞穿底层”的底气。

本系列文章对你有帮助吗?如果喜欢这种深度解析风格,点赞收藏,谢谢!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐