news 2026/10/16 9:15:49

08-并发 Map 的进化史:ConcurrentHashMap 怎么又安全又高效

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
08-并发 Map 的进化史:ConcurrentHashMap 怎么又安全又高效

CAS + 锁桶头,JDK8 把并发粒度细化到桶

HashMap非线程安全、多线程会数据错乱。那并发场景下该用什么?老一辈的答案是Hashtable或Collections.synchronizedMap,但它们都用一把大锁,高并发下吞吐惨不忍睹。ConcurrentHashMap(CHM)才是现代 Java 的标配。它经历了 JDK 7 的分段锁、JDK 8 的"数组+链表/树 + CAS + synchronized 锁单个桶头"两次大重构。这一篇把它的演进和 JDK 8 实现讲透。

一、前辈们的局限:一把大锁

Hashtable在方法上加synchronized:

publicsynchronizedVput(Kkey,Vvalue){...}publicsynchronizedVget(Objectkey){...}

任何读写都抢同一把对象锁,多线程下全是串行,CPU 多核优势完全浪费。Collections.synchronizedMap同理,只是把锁包在方法外。它们"安全"但"慢",在并发度高的缓存、计数场景直接成为瓶颈。

二、JDK 7:分段锁(Segment)

JDK 7 的 CHM 把数据分成 16 个Segment,每个 Segment 是一把独立的 ReentrantLock,内部各是一个小 HashMap:

ConcurrentHashMap ├── Segment[0] (独立锁) → 小 HashMap ├── Segment[1] (独立锁) → 小 HashMap ├── ...(默认 16 段) └── Segment[15]

不同 Segment 的写操作互不阻塞,理论上并发度 16。这比一把大锁进步很大,但仍有问题:① Segment 数固定,扩不了并发度;② 哈希冲突集中在一个 Segment 时,该段仍是瓶颈;③ 内存开销大(每个 Segment 一套结构)。JDK 8 干脆推倒重来。

三、JDK 8:更细的粒度,锁单个桶头

JDK 8 的 CHM 放弃了 Segment,结构和 HashMap 几乎一致(数组桶 + 链表/红黑树),并发控制靠两把武器:

  1. CAS(无锁乐观尝试):用于简单状态变更,比如桶还是 null 时直接 CAS 放节点。
  2. synchronized 锁单个桶的头节点:只在真正发生冲突、要改链表/树时才锁"这一个桶",其他桶完全不受影响。

粒度从"16 个段"细化到"每个桶",并发度 ≈ 桶的数量(远多于 16)。

四、put 流程:CAS 试探 + 锁头节点

简化版putVal逻辑:

finalVputVal(Kkey,Vvalue,booleanonlyIfAbsent){if(key==null||value==null)thrownewNullPointerException();// CHM 不允许 nullinthash=spread(key.hashCode());for(Node<K,V>[]tab=table;;){Node<K,V>f;intn,i,fh;if(tab==null)initTable();// 懒初始化elseif((f=tabAt(tab,i=(n-1)&hash))==null){if(casTabAt(tab,i,null,newNode<>(hash,key,value)))break;// ① 桶为空:CAS 无锁放入}elseif((fh=f.hash)==MOVED)tab=helpTransfer(tab,f);// 正在扩容:协助搬迁else{synchronized(f){// ② 桶非空:锁住这个桶的头节点if(tabAt(tab,i)==f){if(fh>=0){/* 遍历链表,相同 key 覆盖,否则尾插 */...}elseif(finstanceofTreeBin)/* 走红黑树插入 */}}}if(++size>threshold)resize();// 计数用分段计数器,低竞争}returnnull;}

三个关键点:

  • 桶空用 CAS:最常见的"首次放入"完全无锁,性能拉满。
  • 桶非空才 synchronized(f):只锁当前这个头节点,其他桶并行无碍。锁的粒度极小。
  • 不允许 null:key/value 为 null 直接抛 NPE——避免"ConcurrentHashMap 里 null 到底表示’没这个 key’还是’value 就是 null’"的歧义(HashMap 单线程能区分,并发下 get 返回 null 无法判断,故干脆禁止)。

五、size 计数:分段的 CounterCell

HashMap用一个size字段,CHM 多线程下直接++会丢。JDK 8 用一个baseCount+CounterCell[]的分段计数:

privatetransientvolatilelongbaseCount;privatetransientvolatileCounterCell[]counterCells;

线程先 CAS 改baseCount,竞争失败就落到某个CounterCell上各自累加,最后size()把baseCount和所有CounterCell求和。相当于把"一个热点计数器"拆成多个,避免所有线程抢同一变量。所以 CHM 的size()是弱一致的(返回某一刻的近似快照),不是实时精确值——高并发下追求精确 size 本身就没意义,要用mappingCount()当估算。

六、扩容:多线程协助搬迁(helpTransfer)

HashMap 扩容是单线程搬家,CHM 在扩容时允许其他线程一起搬:

  • 某线程 put 时发现f.hash == MOVED(标识正在扩容的ForwardingNode),就调用helpTransfer进去帮忙搬一部分桶,搬完再继续自己的 put。
  • 用transferIndex把桶区间分给不同线程,避免重复搬。
  • 配合"高低位拆分"(同 HashMap),把每个旧桶拆成 低桶/高桶 两条链表,O(1) 重分配。

这让 CHM 在扩容时仍能保持高吞吐,不会因为"一个人搬家、所有人等着"而卡死。

七、为什么读几乎无锁?

get全程几乎不加锁:

publicVget(Objectkey){Node<K,V>[]tab;Node<K,V>e,p;intn,eh;Kek;inth=spread(key.hashCode());if((tab=table)!=null&&(n=tab.length)>0&&(e=tabAt(tab,(n-1)&h))!=null){if((eh=e.hash)==h&&((ek=e.key)==key||(ek!=null&&key.equals(ek))))returne.val;elseif(eh<0)returne.find(h,key);// 树或 ForwardingNode 各自处理while((e=e.next)!=null)// 遍历链表if(e.hash==h&&((ek=e.key)==key||...))returne.val;}returnnull;}

table和节点 value 用volatile修饰(tabAt用Unsafe.getObjectVolatile读),保证可见性。读操作不阻塞写、写操作的锁也很短,所以读多写少场景 CHM 性能极好。这也是它取代 synchronizedMap 当缓存容器的根本原因。

八、和 Hashtable / synchronizedMap 的对比

维度HashtablesynchronizedMapConcurrentHashMap
锁粒度整表一把锁整表一把锁桶头节点 / CAS
读是否阻塞是(方法锁)是几乎无锁
并发度11≈ 桶数
允许 null否是否
迭代器快速失败快速失败弱一致(不抛 CME)
适用淘汰简单同步高并发首选

CHM 的迭代器是弱一致的:遍历时其他线程改了,迭代器可能看到也可能看不到最新值,但绝不抛ConcurrentModificationException,可以边遍历边修改。

九、常见误用

  1. map.putIfAbsent(k, v)不是"检查+创建"的原子组合?它本身就是原子的,常用于单例缓存:map.putIfAbsent(key, compute(key))——注意compute(key)仍然会先执行!要"不存在才计算"请用computeIfAbsent(JDK 8),它只在 key 缺失时才调用函数,避免无谓计算。
  2. get返回 null 不能区分"无 key"和"value 为 null":CHM 直接禁止 null value,别踩。
  3. size() 不精确:高并发别依赖size()==0做业务判断,用isEmpty()也弱一致,需要严格语义请加外部锁或用别的结构。

十、面试追问

  • Q:JDK 7 和 JDK 8 的 CHM 最大区别?7 用 Segment 分段锁(16 段、粒度粗、不可扩并发度);8 去掉 Segment,用 CAS + synchronized 锁单个桶头,粒度更细、并发度更高、内存更省。
  • Q:为什么 JDK 8 用 synchronized 而不是 ReentrantLock?锁的是单个桶头节点、持有时间极短,synchronized 在 JVM 里经过锁升级(偏向→轻量→重量)优化后,这种"极短临界区"性能已不输 ReentrantLock,还省了 AQS 的对象开销。而且锁的粒度已经足够小,没必要上更复杂的锁。
  • Q:CHM 能完全替代 HashMap 吗?单线程优先 HashMap(更轻、允许 null);只有并发才上 CHM。别为了"显得高级"在无并发处用 CHM,反而多了 volatile/CAS 开销。

总结

ConcurrentHashMap 用"CAS 无锁试探 + synchronized 锁单个桶头"把并发粒度细化到桶,读几乎无锁、写互不阻塞;分段 CounterCell 做弱一致计数,多线程协助扩容避免搬家卡顿;JDK 8 相比 JDK 7 的 Segment 分段锁,粒度更细、并发更高、内存更省。它不允许 null、迭代弱一致,是并发键值存储的现代标配,彻底淘汰了 Hashtable 和 synchronizedMap。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 9:15:36

AB Download Manager 浏览器扩展 Edge 兼容性解析与安装路径指南

AB Download Manager 浏览器扩展 Edge 兼容性解析与安装路径指南 【免费下载链接】ab-download-manager A Download Manager that speeds up your downloads 项目地址: https://gitcode.com/GitHub_Trending/ab/ab-download-manager 你打开 Edge 扩展商店搜了一圈&#…

作者头像 李华
网站建设 2026/10/16 9:14:57

CMake集成MuJoCo动力学仿真:从环境配置到步进计算

简介&#xff1a;面向机器人学、运动控制与强化学习方向的C开发者&#xff0c;这份打包资源解决了用CMake组织Mujoco工程并进行简单动力学计算的入门难题。压缩包内共217个文件&#xff0c;约26.29MB&#xff0c;以cmake配置脚本、make构建规则、C源文件与头文件为主体&#xf…

作者头像 李华
网站建设 2026/10/16 9:12:30

ChatGPT 图片生成入门:用文字描述做出可用的配图

ChatGPT 图片生成入门&#xff1a;用文字描述做出可用的配图适合人群&#xff1a;写博客、做 PPT、发公众号时需要配图&#xff0c;但不会设计软件&#xff0c;也不想到处找图担心版权的同学。按 OpenAI 帮助中心的说明&#xff0c;ChatGPT 的图片生成&#xff08;ChatGPT Imag…

作者头像 李华
网站建设 2026/10/16 9:11:37

具身智能开源项目Open-RAIL:VLA/WAM技术栈与上手实践指南

1. 从"向全球开源"这四个字里&#xff0c;我读出了什么第一次看到"向全球开源"这个标题&#xff0c;配合 Open-RAIL、VLA/WAM、具身智能这几个关键词&#xff0c;我的直觉是&#xff1a;这大概率是一个面向机器人领域的开源项目发布&#xff0c;而且野心不…

作者头像 李华