🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐➕关注👀是作者创作的最大动力🤞
💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝+关注👀欢迎留言讨论
🔥🔥🔥(源码获取 + 调试运行 + 问题答疑)🔥🔥🔥 有兴趣可以联系我
🔥🔥🔥 文末有往期免费源码,直接领取获取(无删减,无套路)
我们常常在当下感到时间慢,觉得未来遥远,但一旦回头看,时间已经悄然流逝。对于未来,尽管如此,也应该保持一种从容的态度,相信未来仍有许多可能性等待着我们。
🔥🔥🔥(免费,无删减,无套路):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
提取码:见文章末尾
ConcurrentHashMap JDK8 get()方法无锁设计的深度解析
一、引言:为什么ConcurrentHashMap的get操作可以无锁?
在多线程并发编程中,HashMap的线程安全问题一直是开发者的痛点。传统的解决方案要么使用Collections.synchronizedMap()进行全表锁,要么使用Hashtable这样的线程安全但性能低下的容器。ConcurrentHashMap的出现,彻底改变了这一局面,它通过精细化的锁设计实现了高并发的读写操作。
核心突破:ConcurrentHashMap在JDK8中进行了架构重构,其中最引人注目的改进之一就是get()方法的完全无锁化。这意味着多个线程可以同时执行get操作而无需任何同步,这在读多写少的场景下带来了巨大的性能提升。
二、get()方法源码全景分析
2.1 方法入口与基本流程
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算key的hash值
int h = spread(key.hashCode());
// 第一次检查:表不为空且长度大于0,且对应桶不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 检查第一个节点是否就是目标节点
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// hash值为负表示特殊节点(树节点或迁移节点)
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 遍历链表查找
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
2.2 关键步骤详解
步骤1:hash值的计算与分发
spread()方法不仅仅是简单的hashCode()调用,它进行了一次再散列:
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
这个过程称为"扰动函数",目的是将高位特征分散到低位,减少hash冲突。
步骤2:数组下标的定位
通过(n - 1) & h计算数组下标,这实际上是对数组长度取模的优化版本:
-
要求数组长度n必须是2的幂(ConcurrentHashMap保证这一点)
-
位运算
&比取模运算%快得多
步骤3:volatile读获取桶首节点
tabAt()方法使用Unsafe类进行volatile读:
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
关键点:volatile读保证了内存可见性,确保读线程能看到最新的数据。
三、数据结构与内存可见性保证
3.1 Node节点的设计
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val; // volatile保证值的可见性
volatile Node<K,V> next; // volatile保证next指针的可见性
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
// 其他方法...
}
设计精妙之处:
-
key和hash是final的,创建后不可变 -
val和next是volatile的,保证多线程下的可见性 -
这种设计使得读操作无需锁即可看到正确的数据
3.2 红黑树节点TreeNode
当链表长度超过阈值(默认为8)时,链表会转换为红黑树:
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // 红黑树父节点
TreeNode<K,V> left; // 左子节点
TreeNode<K,V> right; // 右子节点
TreeNode<K,V> prev; // 前驱节点(用于删除)
boolean red; // 红色标记
TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
// 查找方法
final Node<K,V> find(int h, Object k) {
// 红黑树查找逻辑
}
}
四、核心问题:无锁get如何应对并发修改?
4.1 链表遍历的安全性
对于链表结构,即使有线程在并发修改(如插入或删除节点),get操作仍然是安全的:
-
next指针的volatile保证:当添加新节点时,新节点的next指向当前头节点,然后通过CAS将桶首指针指向新节点。这个过程中,get线程要么看到旧链表,要么看到新链表,不会看到中间不一致状态。
-
节点的不可变性:已存在的节点不会被修改(除了删除时的标记),新节点创建后就不可变。
4.2 红黑树查找的并发安全性
这是最复杂的部分:红黑树在平衡操作(旋转)时,会修改多个节点的父子指针关系。那么get操作如何保证不会看到不一致的树结构?
解决方案一:TreeBin的设计
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root; // 红黑树根节点
volatile TreeNode<K,V> first; // 链表头节点
volatile Thread waiter; // 等待线程
volatile int lockState; // 锁状态
// 查找方法:在持有读锁或乐观读的情况下进行
final Node<K,V> find(int h, Object k) {
if (k != null) {
for (Node<K,V> e = first; e != null; ) {
int s; K ek;
// 检查锁状态
if (((s = lockState) & (WAITER|WRITER)) != 0) {
// 有写操作正在进行,使用链表方式查找
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
e = e.next;
}
else if (U.compareAndSwapInt(this, LOCKSTATE, s,
s + READER)) {
// 成功获取读锁,进行树查找
TreeNode<K,V> r, p;
try {
p = ((r = root) == null ? null :
r.findTreeNode(h, k, null));
} finally {
Thread w;
// 释放读锁
if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
(READER|WAITER) && (w = waiter) != null)
LockSupport.unpark(w);
}
return p;
}
}
}
return null;
}
}
解决方案二:读写分离思想
TreeBin内部维护了两个视图:
-
链表视图:所有节点仍以链表形式连接(通过next指针)
-
树视图:红黑树结构(通过left/right/parent指针)
当进行树平衡操作时:
-
写线程(进行旋转操作)需要获取写锁
-
读线程检查到有写操作时,退化为链表查找
-
读线程获取读锁后,可以进行树查找
解决方案三:CAS与乐观读
TreeBin使用CAS操作管理锁状态,实现了乐观读机制:
-
首先尝试乐观读(不获取锁)
-
如果检测到有写操作,退化为链表查找
-
否则,尝试获取读锁进行树查找
4.3 扩容期间的get操作
ConcurrentHashMap支持并发扩容,get操作在扩容期间仍然可以正常进行:
// ForwardingNode的find方法
Node<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;) {
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
关键机制:ForwardingNode是一个特殊节点,当某个桶被迁移后,原位置会被替换为ForwardingNode。get操作遇到这个节点时,会转到新表(nextTable)中继续查找。
五、性能优化细节
5.1 内存屏障的使用
ConcurrentHashMap大量使用Unsafe类来插入内存屏障,确保指令顺序和内存可见性:
-
getObjectVolatile:相当于volatile读 -
compareAndSwapObject:CAS操作,包含完整的内存屏障
5.2 局部变量优化
注意get方法中大量使用局部变量:
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
这样做的好处:
-
减少对堆内存的访问次数
-
编译器可以进行更好的优化
-
避免多线程下的重复计算
5.3 短路判断优化
代码中多次使用短路逻辑判断:
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
先进行引用相等判断,如果不相等再进行equals比较,这种优化在热点路径上能显著提升性能。
六、实际应用建议
6.1 何时使用ConcurrentHashMap
-
读多写少的场景最合适
-
需要高并发访问的缓存
-
需要线程安全的计数器等
6.2 使用注意事项
-
不要依赖size()的精确性:size()方法返回的是估计值
-
迭代器弱一致性:迭代器反映的是创建时的状态
-
key和value不能为null:设计如此,避免二义性
6.3 性能调优参数
-
concurrencyLevel:在JDK8中已变为兼容性参数,实际作用不大 -
initialCapacity:初始容量,根据预估数据量设置 -
loadFactor:负载因子,默认0.75
七、总结
ConcurrentHashMap的get()方法无锁设计是Java并发编程的杰作,它通过多种技术手段的结合实现了高效且安全的并发读取:
-
volatile语义:保证内存可见性
-
CAS操作:实现无锁的原子更新
-
不可变对象:Node节点的key和hash不可变
-
精细化的锁设计:TreeBin的读写锁分离
-
安全的数据结构转换:链表与树的安全转换
这种设计充分体现了"读无锁,写最小锁"的并发优化思想,为高并发场景下的数据访问提供了优秀的解决方案。理解这些底层机制,不仅有助于我们更好地使用ConcurrentHashMap,也能为设计自己的并发数据结构提供宝贵的参考。
ConcurrentHashMap get操作流程图
往期免费源码对应视频:
免费获取--SpringBoot+Vue宠物商城网站系统
🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐➕关注👀是作者创作的最大动力🤞
💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝+关注👀欢迎留言讨论
🔥🔥🔥(源码 + 调试运行 + 问题答疑)
🔥🔥🔥 有兴趣可以联系我
💖学习知识需费心,
📕整理归纳更费神。
🎉源码免费人人喜,
🔥码农福利等你领!💖常来我家多看看,
📕网址:扣棣编程,
🎉感谢支持常陪伴,
🔥点赞关注别忘记!💖山高路远坑又深,
📕大军纵横任驰奔,
🎉谁敢横刀立马行?
🔥唯有点赞+关注成!
⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇点击此处获取源码⬇⬇⬇⬇⬇⬇⬇⬇⬇
7760

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



