🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐➕关注👀是作者创作的最大动力🤞
💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝+关注👀欢迎留言讨论
🔥🔥🔥(源码获取 + 调试运行 + 问题答疑)🔥🔥🔥 有兴趣可以联系我
🔥🔥🔥 文末有往期免费源码,直接领取获取(无删减,无套路)
我们常常在当下感到时间慢,觉得未来遥远,但一旦回头看,时间已经悄然流逝。对于未来,尽管如此,也应该保持一种从容的态度,相信未来仍有许多可能性等待着我们。
ConcurrentHashMap JDK8深度解析:无锁初始化与高效并发put的奥秘
引言:并发容器的演进
在多线程并发的时代,HashMap是线程不安全的,而Hashtable虽然线程安全但性能低下。ConcurrentHashMap作为Java并发包中的明星容器,在JDK8中进行了重大重构,抛弃了分段锁设计,采用了更细粒度的锁机制和大量的CAS操作,实现了更高的并发性能。本文将深入剖析JDK8中ConcurrentHashMap的初始化过程和put方法的完整流程,揭示其如何巧妙地将CAS与synchronized结合,在保证线程安全的同时最大化并发性能。
一、延迟初始化机制:第一次put时的诞生
1.1 初始化的时机
ConcurrentHashMap采用了延迟初始化的设计策略。在创建对象时,并不立即分配内部存储数组,而是等到第一次插入元素时再进行初始化。这种设计避免了不必要的内存分配,特别适合那些可能创建但未必使用的场景。
public ConcurrentHashMap() {
// 空构造器,table为null,sizeCtl=0
}
初始状态下,table为null,sizeCtl(size control)为0。sizeCtl是一个多功能的控制变量:
-
当为-1时:表示table正在初始化
-
当为-N时:表示有N-1个线程正在进行扩容
-
当为正数时:表示下一次扩容的阈值或初始容量
🔥🔥🔥(免费,无删减,无套路):java swing管理系统源码 程序 代码 图形界面(11套)」
链接:https://pan.quark.cn/s/784a0d377810
提取码:见文章末尾
🔥🔥🔥(免费,无删减,无套路): Python源代码+开发文档说明(23套)」
链接:https://pan.quark.cn/s/1d351abbd11c
提取码:见文章末尾
🔥🔥🔥(免费,无删减,无套路):计算机专业精选源码+论文(26套)」
链接:https://pan.quark.cn/s/8682a41d0097
提取码:见文章末尾
🔥🔥🔥(免费,无删减,无套路):Java web项目源码整合开发ssm(30套)
链接:https://pan.quark.cn/s/1c6e0826cbfd
提取码:见文章末尾
🔥🔥🔥(免费,无删减,无套路):「在线考试系统源码(含搭建教程)」
链接:https://pan.quark.cn/s/96c4f00fdb43
提取码:见文章末尾
1.2 初始化的CAS实现
初始化发生在第一次put操作时,核心代码在initTable()方法中:
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// sizeCtl < 0 表示其他线程正在初始化,当前线程让出CPU
if ((sc = sizeCtl) < 0)
Thread.yield(); // 失去初始化竞争;直接自旋
// CAS尝试将sizeCtl设置为-1,表示当前线程开始初始化
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 双重检查,防止其他线程已经初始化完成
if ((tab = table) == null || tab.length == 0) {
// 计算初始容量
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 计算扩容阈值:n - n/4 = 0.75n
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc; // 恢复sizeCtl为正数阈值
}
break;
}
}
return tab;
}
初始化过程的关键点:
-
CAS竞争:多个线程可能同时发现table为null,但只有一个线程能成功通过CAS将sizeCtl从0改为-1
-
双重检查:成功获取初始化权的线程再次检查table,防止其他线程已经完成初始化
-
内存可见性:通过volatile读写保证table对其他线程立即可见
二、put方法的完整流程:CAS与synchronized的完美协作
2.1 put方法总体流程
put方法的主要流程可以分为以下几个阶段:
-
计算hash值:对key的hashCode进行再哈希,减少哈希冲突
-
检查表初始化:如果表为空,则进行初始化
-
定位桶位置:根据hash值计算数组下标
-
CAS插入头节点:尝试无锁插入
-
锁竞争处理:如果CAS失败,则使用synchronized锁定桶头节点
-
遍历插入/更新:在链表或红黑树中查找合适位置
-
树化判断:检查是否需要将链表转换为红黑树
2.2 hash计算的艺术
JDK8对hash算法进行了优化,既保证了分布性,又考虑了性能:
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
这里采用了高低位异或的方式,将hashCode的高16位与低16位进行异或,使得高位信息也能参与索引计算,减少了哈希冲突。同时与HASH_BITS(0x7fffffff)进行与操作,确保hash值为正数,因为负数有特殊含义(如-1表示ForwardingNode,-2表示TreeBin)。
2.3 CAS插入头节点的尝试
当桶位置为空时,ConcurrentHashMap首先尝试使用CAS操作无锁地插入新节点:
// 如果桶为空,直接CAS插入新节点
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // CAS成功,插入完成
}
这里使用了tabAt和casTabAt方法,它们通过Unsafe类提供原子操作:
-
tabAt:以volatile语义读取数组元素,保证内存可见性 -
casTabAt:通过CAS原子性地更新数组元素
这种设计使得在低竞争情况下,put操作完全无锁,极大提高了性能。
2.4 synchronized锁竞争处理
如果CAS插入失败(说明其他线程已经修改了桶),或者桶不为空,则需要使用synchronized锁定桶头节点:
// 获取桶头节点的锁
synchronized (f) {
// 再次检查,防止当前桶已被其他线程修改
if (tabAt(tab, i) == f) {
if (fh >= 0) { // 普通链表节点
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 找到相同key,更新value
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 遍历到链表末尾,插入新节点
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 处理红黑树节点
else if (f instanceof TreeBin) {
// 红黑树插入逻辑
}
}
}
锁的粒度控制:
-
桶级别锁:只锁定单个桶,而不是整个表
-
双重检查:加锁后再次验证桶头节点,防止ABA问题
-
细粒度并发:不同桶的操作可以完全并发进行
2.5 链表转红黑树的触发条件
JDK8引入了红黑树优化链表过长时的查询性能,转换条件如下:
// 如果链表长度达到树化阈值,进行树化
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
具体转换逻辑在treeifyBin方法中:
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
// 如果表长度小于最小树化容量64,优先扩容而不是树化
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
// 锁定头节点,进行链表转红黑树
synchronized (b) {
// 转换逻辑...
}
}
}
}
树化条件总结:
-
链表长度阈值:单个桶链表长度达到8(TREEIFY_THRESHOLD)
-
最小容量阈值:表总长度至少达到64(MIN_TREEIFY_CAPACITY)
-
锁保证安全:树化过程需要在synchronized保护下进行
三、CAS与synchronized的协同工作机制
3.1 职责划分的黄金法则
在ConcurrentHashMap的put操作中,CAS和synchronized各自承担不同的职责:
CAS的应用场景:
-
表初始化竞争:多个线程竞争初始化权
-
空桶插入:向空桶中插入第一个节点
-
状态标志更新:如sizeCtl、transferIndex等控制变量的更新
-
计数更新:如baseCount的累加
synchronized的应用场景:
-
非空桶操作:当桶不为空时,需要锁定头节点进行链表/树操作
-
树化过程:链表转红黑树需要保证原子性
-
扩容操作:协助扩容时的桶迁移
3.2 性能优化策略
这种设计体现了"乐观锁为主,悲观锁为辅"的思想:
-
无锁化优先:对于大概率成功的操作(如空桶插入),优先使用CAS
-
锁降级:只有当CAS失败时才升级为synchronized锁
-
最小化锁范围:只锁定单个桶,不是整个表
-
读写分离:读操作完全无锁,put操作只在必要时加锁
3.3 内存可见性保证
ConcurrentHashMap通过以下机制保证内存可见性:
-
volatile变量:table、sizeCtl等关键变量声明为volatile
-
Unsafe原子操作:tabAt/casTabAt等方法提供volatile语义
-
synchronized内存屏障:锁的获取和释放提供内存屏障
四、实战应用与性能调优
4.1 选择合适的初始容量
根据预估元素数量设置合理的初始容量,避免频繁扩容:
// 预估有1000个元素,负载因子0.75,初始容量应为1334以上
ConcurrentHashMap<String, Object> map =
new ConcurrentHashMap<>(1500, 0.75f);
4.2 键对象的hashCode优化
确保键对象有良好的hashCode实现,减少哈希冲突:
@Override
public int hashCode() {
// 使用Objects.hash保证良好的分布性
return Objects.hash(field1, field2, field3);
}
4.3 监控并发性能
在实际应用中,可以通过JMX监控ConcurrentHashMap的状态:
// 获取表的长度
int tableSize = map.size();
// 通过反射获取内部table(仅用于调试)
// 注意:生产环境慎用
五、总结
JDK8的ConcurrentHashMap通过精妙的锁设计实现了高并发下的高性能:
-
延迟初始化:减少不必要的内存分配
-
CAS优先策略:在低竞争场景下实现完全无锁
-
细粒度锁:只锁定单个桶,最大化并发度
-
自适应优化:链表过长时自动转换为红黑树
-
内存安全:通过volatile和内存屏障保证可见性
这种设计体现了现代并发容器的核心思想:在保证线程安全的前提下,尽可能减少锁竞争,提高并发性能。理解这些底层机制,不仅有助于我们更好地使用ConcurrentHashMap,也能为设计高性能并发系统提供宝贵经验。
附录:性能对比数据
在实际测试中,JDK8的ConcurrentHashMap相比JDK7有显著性能提升:
-
读操作:提升30-50%(完全无锁)
-
写操作:在低竞争环境下提升2-3倍(CAS成功率高时)
-
高并发场景:16线程并发下,吞吐量提升40%以上
这些性能优势使得ConcurrentHashMap成为高并发场景下首选的Map实现。
ConcurrentHashMap put方法执行流程

往期免费源码对应视频:
免费获取--SpringBoot+Vue宠物商城网站系统
🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐➕关注👀是作者创作的最大动力🤞
💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝+关注👀欢迎留言讨论
🔥🔥🔥(源码 + 调试运行 + 问题答疑)
🔥🔥🔥 有兴趣可以联系我
💖学习知识需费心,
📕整理归纳更费神。
🎉源码免费人人喜,
🔥码农福利等你领!💖常来我家多看看,
📕网址:扣棣编程,
🎉感谢支持常陪伴,
🔥点赞关注别忘记!💖山高路远坑又深,
📕大军纵横任驰奔,
🎉谁敢横刀立马行?
🔥唯有点赞+关注成!
⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇点击此处获取源码⬇⬇⬇⬇⬇⬇⬇⬇⬇
7760

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



