好的,这是一篇根据您的要求撰写的关于ConcurrentHashMap高并发设计的文章,结合了最新的技术解读,符合优快云社区的高质量风格。
ConcurrentHashMap高并发之道:深度解析JDK8+源码级分片与多线程协同扩容
关键词: ConcurrentHashMap, 高并发, Java并发, 源码分析, 分段锁, CAS, 扩容机制
引言
在Java并发编程的殿堂中,ConcurrentHashMap(以下简称CHM)无疑是颗璀璨的明珠。它完美解决了Hashtable和Collections.synchronizedMap因全局锁导致的性能瓶颈,成为高并发场景下键值对容器的首选。JDK8对CHM进行了颠覆性的重写,摒弃了JDK7中基于Segment的分段锁技术,采用了更精妙的“CAS + synchronized”实现,将并发性能提升到了新的高度。本文将深入源码,为你揭示CHM在分片策略与扩容机制上的高并发设计哲学。
一、分片策略的演进:从分段锁到细粒度锁
1.1 JDK7的Segment分段锁回顾
在JDK7中,CHM采用了“分段锁”的策略。其内部维护了一个Segment数组,每个Segment本身就是一个继承自ReentrantLock的哈希表。进行put、get等操作时,会先根据key的哈希值定位到具体的Segment,然后只对该Segment加锁。这种设计使得不同Segment的操作可以并行,在一定程度上了提高了并发度。
局限性: 锁的粒度虽然比全局锁细,但仍然是“一段”数据(一个哈希桶数组)共享一把锁。当线程集中访问同一个Segment时,竞争依然激烈。Segment的数量在创建时就已固定,缺乏灵活性。
1.2 JDK8+的革命性设计:粒度到桶级别的锁
JDK8的CHM彻底抛弃了Segment,其并发控制思想发生了根本转变:
- 数据结构变更: 内部采用与
HashMap相似的Node数组 + 链表 / 红黑树 结构。
- 核心思想: 尽可能使用无锁操作,仅在必须时进行最小范围的加锁。
源码级分片策略:
分片的基础是Node数组table。当需要插入或更新一个键值对时,CHM通过以下步骤确定“片”的位置:
java
// 根据key的哈希值计算在数组中的下标
int hash = spread(key.hashCode());
int i = (table.length - 1) & hash; // 定位到具体的桶(分片)
Node<K,V> f = tabAt(tab, i); // 使用volatile语义获取桶的头节点
这里的“分片”不再是固定的Segment,而是动态的、粒度更细的单个哈希桶。每个桶(即table数组的一个元素)在逻辑上是一个独立的分片。
锁的施加:
当发生哈希冲突,需要向链表或树中插入节点时,CHM并不会直接锁住整个Map或一片区域,而是使用synchronized关键字只锁住当前桶的头节点。
java
// 在putVal方法中
synchronized (f) { // f是桶i的头节点
if (tabAt(tab, i) == f) {
// ... 在锁内进行链表或树的插入操作
}
}
这种设计的精妙之处在于:
1. 极高并发度: 只要线程访问的是不同的哈希桶,它们的操作就完全不会阻塞,可以真正并行。
2. 动态灵活: 锁的粒度与哈希桶绑定,随着table的扩容,锁的粒度会自动调整,更加合理。
二、精妙绝伦的多线程协同扩容机制
扩容是CHM设计中最复杂、也最体现其高并发水准的部分。它不再是单个线程的“苦力活”,而是演变成一场多线程协同的“团体舞”。
2.1 触发扩容的时机
当CHM中的元素数量超过sizeCtl(一个非常重要的控制变量)所设定的阈值时,就会触发扩容。sizeCtl是一个 volatile 变量,它扮演着多种角色:初始化控制、扩容阈值、扩容线程数控制等。
2.2 扩容的核心过程:transfer方法
扩容的核心逻辑在private final void transfer(Node<K,V>[] tab, int nextTab)方法中。其核心思想是“任务分治”。
初始化新数组: 第一个触发扩容的线程会创建一个容量为原数组2倍的新数组
nextTable。
任务划分: 扩容不是简单地将每个元素重新计算哈希放入新数组,而是将旧数组
table划分为多个大小相等的“步长”。每个线程负责迁移其中的一个或多个步长。
并发迁移:
- CHM维护一个
transferIndex变量,表示下一个待分配迁移任务的区间的起始下标。初始值为table.length。
- 当一个线程(比如线程A)参与扩容时,它会通过CAS操作原子性地“领取”一个迁移区间(比如从
transferIndex开始向前数stride个长度的桶)。
- 线程A然后开始逆向处理(从高下标到低下标)自己领到的这个区间内的所有桶。
- 处理每个桶时,会锁住当前桶的头节点,然后将桶中的节点根据哈希值的高位比特(
(hash & oldTable.length))分为两类:一类是留在新数组原位置(i)的,另一类是移动到新位置(i + oldTable.length)的。最后将构建好的两条链表分别放入新数组的对应位置。
- 线程A处理完自己领到的区间后,会继续尝试领取新的区间,直到所有桶都被迁移完毕。
ForwardingNode的作用: 这是一个特殊的节点。当一个桶被成功迁移后,会在旧数组的该位置放置一个
ForwardingNode节点。这个节点有两个关键作用:
- 标识: 告诉其他线程,这个桶已经被迁移过了,无需再处理。
- 路由: 在扩容期间,如果有其他线程进行get或put操作,遇到
ForwardingNode,它就知道去新的nextTable中查找或插入数据。这保证了扩容期间数据的可用性。
2.3 协同工作的魅力
通过这种设计,多个线程可以同时参与扩容,每个线程负责不同的数据区间,极大地加快了扩容速度。sizeCtl的高位比特用来记录正在参与扩容的线程数,低16位记录扩容标识戳,通过CAS进行原子更新,确保了并发控制的安全性和高效性。
总结与最佳实践
JDK8+的ConcurrentHashMap通过“CAS + synchronized”将锁的粒度细化到单个哈希桶,并通过精妙的多线程协同扩容机制,实现了在高并发环境下近乎极致的性能。
- 分片策略: 从固定的
Segment分片升级为动态的、以哈希桶为单位的细粒度分片,大大减少了锁竞争。
- 扩容机制: 从单线程扩容升级为多线程协同并行扩容,将扩容时间对性能的影响降到了最低。
最佳实践:
- 在初始化CHM时,如果能预估数据量,应尽量指定初始容量,避免频繁扩容。
- 理解其并发原理,有助于在更复杂的并发场景下正确、高效地使用CHM。
CHM的设计是工程智慧与算法之美结合的典范,深入理解其源码,对于每一位致力于提升技术深度的Java开发者而言,都是一次宝贵的学习经历。
参考资料:
1. Oracle官方JDK 17源码 - java.util.concurrent.ConcurrentHashMap
2. 美团技术博客 - 《Java 8系列之重新认识HashMap》
3. 优快云社区多位专家的源码分析文章
希望这篇深入源码的分析能帮助你更好地理解ConcurrentHashMap的高并发设计。欢迎在评论区交流讨论!
1万+

被折叠的 条评论
为什么被折叠?



