连载中 16/21

ConcurrentHashMap:从分段锁到 CAS + synchronized

2026-10-30 · 34 阅读 · 0 评论 · 0 赞

为什么不能并发用 HashMap

两个血案。JDK 7:扩容时并发操作,链表头插法把节点指成了环,之后 get 这个桶就是CPU 100% 的死循环(面试题常客);JDK 8 换成尾插修掉了成环问题,但并发 put 依然会丢数据——两个线程同时写同一个空桶,一个覆盖另一个。补救办法里,Hashtable 全表一把锁太笨重,Collections.synchronizedMap 同样全表锁;ConcurrentHashMap 才是正解,而且它的锁粒度进化史,本身就是一部并发优化教材。

JDK 7:分段锁,16 段的折中

JDK 7 把整个 map 切成 16 个 Segment(小 HashMap),每个段自己一把锁:写不同的段互不干扰,并发度 16。比全表锁强多了,但粒度依然粗——两个 key 落在不同桶、同一分段,也得排队。16 是默认并发度,可配但要初始化时定死,改起来麻烦。

JDK 8:把锁做到单个桶

JDK 8 推倒重来:数组 + 链表 + 红黑树的结构(树化阈值 8、退化阈值 6),锁粒度直接砍到单个桶。put 的判定链:

1. 桶是空的          → CAS 放入新节点,无锁成功          // 大多数场景到这就结束
2. 桶头是 ForwardingNode → 正在扩容,先帮忙搬数据
3. 桶上有节点        → synchronized 锁住桶头,链表/红黑树里插入

真正的写冲突(同一桶)才加锁,且锁的是桶头对象——并发度约等于数组长度,而不是写死的 16。至于为什么用 synchronized 不用 ReentrantLock:锁的是单个桶头、冲突概率低,偏向/轻量级锁的路径就够用(第 4 篇的锁升级在这兑现价值),而且 synchronized 不占对象额外的内存头。

size 统计:LongAdder 的亲兄弟

拿 CAS 维护一个全局 size,在高并发下就是 CAS 失败风暴。CHM 的解法与第 8 篇的 LongAdder 同宗:base 加 CounterCell 数组,冲突时分摊到各自的 Cell。同一个思路在两个容器里各长了一遍——并发优化的招式是通用的。

迭代是弱一致的

HashMap 迭代途中被改,直接抛 ConcurrentModificationException;CHM 的迭代器是弱一致的:不抛异常,迭代开始后新写入的数据可能看到也可能看不到。所以别指望「边遍历边修改」有精确语义——需要「取出并删除」这类原子组合操作,用 putIfAbsent、computeIfAbsent、remove(key, value) 这些方法,而不是先 get 再 put 两步走。

下一篇补全并发容器拼图:CopyOnWriteArrayList 的写时复制,以及 BlockingQueue 家族——生产者消费者的现代标准答案。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#Java#ConcurrentHashMap#分段锁#红黑树#JUC

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10113 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9876 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9306 阅读 · 21 评论 · 287 赞