解析ConcurrentHashMap:get()方法无锁设计

编程达人挑战赛·第5期 10w+人浏览 390人参与

🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐➕关注👀是作者创作的最大动力🤞

💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝+关注👀欢迎留言讨论

🔥🔥🔥(源码获取 + 调试运行 + 问题答疑)🔥🔥🔥  有兴趣可以联系我

🔥🔥🔥  文末有往期免费源码,直接领取获取(无删减,无套路)

我们常常在当下感到时间慢,觉得未来遥远,但一旦回头看,时间已经悄然流逝。对于未来,尽管如此,也应该保持一种从容的态度,相信未来仍有许多可能性等待着我们。

🔥🔥🔥(免费,无删减,无套路):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;
     }
     
     // 其他方法...
 }

设计精妙之处

  1. keyhash是final的,创建后不可变

  2. valnext是volatile的,保证多线程下的可见性

  3. 这种设计使得读操作无需锁即可看到正确的数据

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操作仍然是安全的:

  1. next指针的volatile保证:当添加新节点时,新节点的next指向当前头节点,然后通过CAS将桶首指针指向新节点。这个过程中,get线程要么看到旧链表,要么看到新链表,不会看到中间不一致状态。

  2. 节点的不可变性:已存在的节点不会被修改(除了删除时的标记),新节点创建后就不可变。

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内部维护了两个视图:

  1. 链表视图:所有节点仍以链表形式连接(通过next指针)

  2. 树视图:红黑树结构(通过left/right/parent指针)

当进行树平衡操作时:

  • 写线程(进行旋转操作)需要获取写锁

  • 读线程检查到有写操作时,退化为链表查找

  • 读线程获取读锁后,可以进行树查找

解决方案三:CAS与乐观读

TreeBin使用CAS操作管理锁状态,实现了乐观读机制:

  1. 首先尝试乐观读(不获取锁)

  2. 如果检测到有写操作,退化为链表查找

  3. 否则,尝试获取读锁进行树查找

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;

这样做的好处:

  1. 减少对堆内存的访问次数

  2. 编译器可以进行更好的优化

  3. 避免多线程下的重复计算

5.3 短路判断优化

代码中多次使用短路逻辑判断:

if ((ek = e.key) == key || (ek != null && key.equals(ek)))

先进行引用相等判断,如果不相等再进行equals比较,这种优化在热点路径上能显著提升性能。

六、实际应用建议

6.1 何时使用ConcurrentHashMap

  1. 读多写少的场景最合适

  2. 需要高并发访问的缓存

  3. 需要线程安全的计数器等

6.2 使用注意事项

  1. 不要依赖size()的精确性:size()方法返回的是估计值

  2. 迭代器弱一致性:迭代器反映的是创建时的状态

  3. key和value不能为null:设计如此,避免二义性

6.3 性能调优参数

  1. concurrencyLevel:在JDK8中已变为兼容性参数,实际作用不大

  2. initialCapacity:初始容量,根据预估数据量设置

  3. loadFactor:负载因子,默认0.75

七、总结

ConcurrentHashMap的get()方法无锁设计是Java并发编程的杰作,它通过多种技术手段的结合实现了高效且安全的并发读取:

  1. volatile语义:保证内存可见性

  2. CAS操作:实现无锁的原子更新

  3. 不可变对象:Node节点的key和hash不可变

  4. 精细化的锁设计:TreeBin的读写锁分离

  5. 安全的数据结构转换:链表与树的安全转换

这种设计充分体现了"读无锁,写最小锁"的并发优化思想,为高并发场景下的数据访问提供了优秀的解决方案。理解这些底层机制,不仅有助于我们更好地使用ConcurrentHashMap,也能为设计自己的并发数据结构提供宝贵的参考。

ConcurrentHashMap get操作流程图



往期免费源码对应视频:

免费获取--SpringBoot+Vue宠物商城网站系统

🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐➕关注👀是作者创作的最大动力🤞

💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝+关注👀欢迎留言讨论

🔥🔥🔥(源码 + 调试运行 + 问题答疑)

🔥🔥🔥  有兴趣可以联系我

💖学习知识需费心,
📕整理归纳更费神。
🎉源码免费人人喜,
🔥码农福利等你领!

💖常来我家多看看,
📕网址:扣棣编程
🎉感谢支持常陪伴,
🔥点赞关注别忘记!

💖山高路远坑又深,
📕大军纵横任驰奔,
🎉谁敢横刀立马行?
🔥唯有点赞+关注成!

⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇点击此处获取源码⬇⬇⬇⬇⬇⬇⬇⬇⬇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值