深度拆解 ConcurrentHashMap 源码:并发容器的演进与锁设计的巅峰
前言
普通的 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.7 的 Segment 还是 1.8 的 Node,核心思想都是减小锁的粒度。
-
乐观无锁:大量使用 CAS 进行初始化和空桶操作。
-
协同合作:扩容不再是一个人的战斗,而是所有线程“多点协作”。
-
动态转换:链表与红黑树的切换保证了极端情况下的查询底线。
结语:通过这几篇长文,我们从最简单的数组聊到了最复杂的并发容器。源码不是为了背诵,而是为了在面对复杂的系统架构时,你能拥有那种“洞穿底层”的底气。
本系列文章对你有帮助吗?如果喜欢这种深度解析风格,点赞收藏,谢谢!
更多推荐



所有评论(0)