ConcurrentHashMap 为什么比 HashMap 安全?

张开发
2026/5/6 1:54:26 15 分钟阅读
ConcurrentHashMap 为什么比 HashMap 安全?
点击上方“java大数据修炼之道”, 选择“设为星标”技术干货发文后 第一时间奉上引言上一篇我们深入分析了 HashMap 的源码今天来聊聊它的线程安全版本 ConcurrentHashMap。很多人知道HashMap 线程不安全多线程用 ConcurrentHashMap但为什么不安全ConcurrentHashMap 又是怎么解决的今天我们从原理层面彻底搞清楚。一、HashMap 为什么线程不安全HashMap 的线程不安全问题主要体现在以下几个场景1. 扩容时死循环JDK 7JDK 7 的 HashMap 在扩容时使用头插法转移链表节点。多线程同时触发扩容时两个线程可能同时操作同一条链表导致链表形成环形结构。一旦出现环形链表后续的 get 操作就会陷入死循环CPU 飙升到 100%。这是 JDK 7 最经典的 HashMap 并发 bug。JDK 8 改用尾插法解决了死循环问题但仍然存在数据丢失的风险。2. 数据丢失两个线程同时执行 put 操作如果两个 key 的 hash 值相同都判断该位置为空然后都往同一个位置写入后写入的会覆盖先写入的导致数据丢失。3. size 不准确HashMap 的 size 字段没有任何同步保护多线程并发 put 时size 的自增操作不是原子的最终 size 的值会比实际元素数量少。二、ConcurrentHashMap 的演进ConcurrentHashMap 在 JDK 7 和 JDK 8 中的实现方式有很大差异理解这个演进过程非常重要。JDK 7分段锁SegmentJDK 7 的 ConcurrentHashMap 采用分段锁设计。整个 Map 被分成若干个 Segment每个 Segment 继承自 ReentrantLock相当于一个小的 HashMap。默认有 16 个 Segment也就是说最多支持 16 个线程并发写入每个线程操作不同的 Segment。读操作不加锁写操作只锁住对应的 Segment而不是整个 Map。这比直接用 synchronized 锁住整个 HashMap即 Hashtable 的做法性能好很多但 Segment 数量固定并发度有上限。JDK 8CAS synchronizedJDK 8 彻底重写了 ConcurrentHashMap抛弃了分段锁改用 CAS synchronized 的组合方案并发粒度细化到每个数组槽位桶。核心思路数组初始化用 CAS 保证只有一个线程完成初始化插入新节点桶为空用 CAS 直接写入不加锁插入节点桶不为空用 synchronized 锁住该桶的头节点扩容多线程协作扩容每个线程负责一段区间这样锁的粒度从 Segment默认 16 个细化到了每个桶数组长度默认 16最大可达 2 的 30 次方并发性能大幅提升。三、JDK 8 put 操作详解理解 put 操作是理解 ConcurrentHashMap 线程安全的关键。整个流程如下计算 key 的 hash 值如果数组还没初始化调用 initTable() 用 CAS 初始化根据 hash 定位到数组下标 i如果该位置为 null用 CAS 直接插入新节点成功则结束如果该位置正在扩容节点的 hash 值为 MOVED当前线程帮助扩容否则用 synchronized 锁住该位置的头节点然后遍历链表或红黑树插入插入后检查链表长度超过 8 则转为红黑树用 addCount() 更新元素数量内部用 LongAdder 思想分散计数减少竞争四、get 操作为什么不加锁ConcurrentHashMap 的 get 操作全程不加锁这是怎么保证线程安全的关键在于 Node 节点的 val 和 next 字段都用 volatile 修饰。volatile 保证了可见性一个线程修改了节点的值其他线程能立即看到最新值不会读到脏数据。数组本身也用 volatile 修饰通过 Unsafe 的 getObjectVolatile 读取保证扩容后新数组对所有线程可见。五、size() 方法的实现JDK 8 的 ConcurrentHashMap 没有用一个简单的 int 来记录元素数量而是借鉴了 LongAdder 的思想有一个 baseCount 字段低并发时直接用 CAS 更新高并发时更新失败的线程会把增量写入 CounterCell 数组的某个槽位size() 是把 baseCount 和所有 CounterCell 的值加起来这样避免了多线程竞争同一个计数器大幅减少了 CAS 失败的次数。六、ConcurrentHashMap vs Hashtable vs Collections.synchronizedMapHashtable所有方法都加 synchronized锁的是整个对象并发度最低基本已被淘汰。Collections.synchronizedMap包装 HashMap所有操作加同一把锁和 Hashtable 类似并发度低。ConcurrentHashMap锁粒度细化到桶级别读不加锁写只锁单个桶并发性能最好是多线程场景的首选。七、使用注意事项ConcurrentHashMap 虽然线程安全但有几个坑要注意复合操作不是原子的先 get 再 put 这种操作不是原子的多线程下仍然可能出问题。应该用 putIfAbsent、computeIfAbsent 等原子方法不允许 null 键和 null 值HashMap 允许 null但 ConcurrentHashMap 不允许插入 null 会抛 NullPointerExceptionsize() 不是强一致的size() 返回的是一个估算值在并发修改时可能不准确总结HashMap 线程不安全的根本原因是没有任何同步机制多线程并发操作会导致死循环、数据丢失、size 不准等问题。ConcurrentHashMap 通过 CAS synchronized 的组合将锁粒度细化到单个桶读操作完全不加锁写操作只锁单个桶在保证线程安全的同时实现了高并发性能。记住一句话单线程用 HashMap多线程用 ConcurrentHashMap别用 Hashtable。end往期精彩文章复习回顾1.SpringBoot 插件化开发模式真香啊 2.一行代码实现请假审批流程Java版 3.血泪教训8 个线程池最佳实践和坑 4.SpringBoot骚操作一个注解秒杀所有类型的文件下载 5.Controller层代码这么写同事们都模仿起来了最近整理一份资料《程序员学习手册》覆盖了 Java技术、面试题精选、操作系统基础知识、计算机基础知识、Linux教程、计算机网络等等。获取方式点“在看关注公众号Java大数据修炼之道并回复PDF领取更多内容陆续奉上。长按识别下方二维码关注后回复关键字:PDF领取你想学的java知识这里都有,长按下方图片识别关注我们吧~如喜欢本文请点击右上角把文章分享到朋友圈 因公众号更改推送规则请点“在看”并加“星标”第一时间获取精彩技术分享 点分享点收藏点在看

更多文章