JDK 源码解析:ConcurrentHashMap 并发安全实现原理——CAS 乐观锁与 synchronized 局部锁的协同设计

发布时间:2026/9/12 17:32:40
JDK 源码解析:ConcurrentHashMap 并发安全实现原理——CAS 乐观锁与 synchronized 局部锁的协同设计 JDK 源码解析ConcurrentHashMap 并发安全实现原理——CAS 乐观锁与 synchronized 局部锁的协同设计【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter本文基于 JDK 1.8 的ConcurrentHashMap源码剖析它如何在继承 HashMap 哈希桶数组 链表/红黑树数据结构的基础上通过CAS 乐观锁 synchronized 局部锁组合方案解决并发安全与高吞吐之间的矛盾空桶写入不加锁、hash 冲突桶内加锁、扩容期间多线程协助搬迁。读完你将掌握 JDK 1.8 ConcurrentHashMap 的 put 全流程、锁粒度设计、与 JDK 1.7 分段锁Segment ReentrantLock的演进对比以及它在实际框架中的典型应用场景。一、为什么要看 ConcurrentHashMap 的并发实现在 HashMap 源码解析 中我们重点了解了它的核心源码与实现逻辑底层是动态数组数组中元素存放的是链表或红黑树默认初始容量 16、扩容因子 0.75、链表长度达到 8 转红黑树TREEIFY_THRESHOLD并梳理了put、get、resize的核心流程。ConcurrentHashMap 与 HashMap 在数据结构上几乎相差无几因此这里不再重复那些数据结构相关的内容重点看它并发安全的实现。两者在数据结构层面的共同基础成员说明transient volatile NodeK,V[] table哈希桶数组实际存放键值对的地方ConcurrentHashMap 额外加了volatile保证多线程可见性MAXIMUM_CAPACITY 1 30最大容量DEFAULT_CAPACITY 16默认初始容量LOAD_FACTOR 0.75f扩容因子使用容量达到当前容量的 75% 就扩容TREEIFY_THRESHOLD 8链表转红黑树的阈值NodeK,V单向链表节点hash、key、value、next与 HashMap 一致TreeBin/TreeNode红黑树结构桶内冲突元素超过阈值后树化同样地ThreadLocal 源码解析 中也提到看过 HashMap 或 ConcurrentHashMap 源码的同学会对INITIAL_CAPACITY 16、Entry[] table、size这些哈希表的基础设计感到很眼熟——哈希表哈希桶 冲突链的骨架在整个 JDK 中是一脉相承的。既然数据结构同源ConcurrentHashMap 的核心价值就在于如何在多线程并发读写时保证线程安全同时不牺牲吞吐量。这正是本文要深入的部分。二、常量与成员变量的设计源码开头的常量设计与 HashMap 相差无几但有几个值得注意的点/** * 最大容量 */ private static final int MAXIMUM_CAPACITY 1 30; /** * 默认初始容量 */ private static final int DEFAULT_CAPACITY 16; /** * 单个数组最大容量 */ static final int MAX_ARRAY_SIZE Integer.MAX_VALUE - 8; /** * 默认并发等级也就分成多少个单独上锁的区域 */ private static final int DEFAULT_CONCURRENCY_LEVEL 16; /** * 扩容因子 */ private static final float LOAD_FACTOR 0.75f; /** * 哈希桶数组volatile 保证多线程下的可见性 */ transient volatile NodeK,V[] table; /** * 扩容时使用的新数组volatile 保证可见性 */ private transient volatile NodeK,V[] nextTable;table/nextTable被声明为volatile这是与 HashMap 最大的声明差异之一。多线程并发环境下一个线程对数组引用的修改必须对其他线程立即可见volatile保证了这一点配合 CAS 提供的内存屏障语义支撑起无锁读写的安全。DEFAULT_CONCURRENCY_LEVEL 16这个常量沿用了 JDK 1.7 分段锁时代默认并发等级的概念即默认分成多少个单独上锁的区域。在 JDK 1.8 中它主要用于构造方法中的容量估算逻辑并不再像 1.7 那样真正切分 Segment。MAX_ARRAY_SIZE Integer.MAX_VALUE - 8部分 JVM 实现中数组对象头会占用一部分内存预留 8 作为安全余量避免OutOfMemoryError。构造方法依然推荐初始化时根据实际情况设置好初始容量public ConcurrentHashMap() { } public ConcurrentHashMap(int initialCapacity) { if (initialCapacity 0) throw new IllegalArgumentException(); int cap ((initialCapacity (MAXIMUM_CAPACITY 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity (initialCapacity 1) 1)); this.sizeCtl cap; } public ConcurrentHashMap(Map? extends K, ? extends V m) { this.sizeCtl DEFAULT_CAPACITY; putAll(m); } public ConcurrentHashMap(int initialCapacity, float loadFactor) { this(initialCapacity, loadFactor, 1); } public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor 0.0f) || initialCapacity 0 || concurrencyLevel 0) throw new IllegalArgumentException(); if (initialCapacity concurrencyLevel) // Use at least as many bins initialCapacity concurrencyLevel; // as estimated threads long size (long)(1.0 (long)initialCapacity / loadFactor); int cap (size (long)MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)size); this.sizeCtl cap; }要点说明sizeCtl承担了双重身份构造阶段它被用来暂存计算后的初始容量2 的幂次运行阶段它又作为扩容控制标识负数表示正在初始化或扩容正数表示下一次触发扩容的阈值。这与 HashMap 构造方法中先存 threshold、初始化时再取出的思路异曲同工。三个参数的构造方法ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)保留了 1.7 时代并发等级入参的兼容性。其中if (initialCapacity concurrencyLevel) initialCapacity concurrencyLevel;的含义是至少保证桶的数量不少于预估线程数从而让并发线程尽量分散到不同桶上减少锁竞争。容量向上取整到 2 的幂tableSizeFor(...)与 HashMap 一致保证(n - 1) hash的散列定位计算高效且分布均匀。仍然推荐在初始化时根据实际情况设置好初始容量合适的初始容量可以显著减少resize扩容次数避免扩容期间多线程协助搬迁带来的开销提升整体效率。三、put 流程并发安全的核心战场ConcurrentHashMap的核心就在于put元素时利用synchronized 局部锁和CAS 乐观锁机制大大提升了本集合的并发能力比 JDK 7 的分段锁性能更强。public V put(K key, V value) { return putVal(key, value, false); } final V putVal(K key, V value, boolean onlyIfAbsent) { if (key null || value null) throw new NullPointerException(); int hash spread(key.hashCode()); int binCount 0; for (NodeK,V[] tab table;;) { NodeK,V f; int n, i, fh; if (tab null || (n tab.length) 0) tab initTable(); else if ((f tabAt(tab, i (n - 1) hash)) null) { // 注意这是一个CAS的方法将新节点放入指定位置不用加锁阻塞线程 // 也能保证并发安全 if (casTabAt(tab, i, null, new NodeK,V(hash, key, value, null))) break; // no lock when adding to empty bin } // 当前Map在扩容先协助扩容再更新值 else if ((fh f.hash) MOVED) tab helpTransfer(tab, f); else { // hash冲突 V oldVal null; // 局部锁有效减少锁竞争的发生 synchronized (f) { // f 是 链表头节点/红黑树根节点 if (tabAt(tab, i) f) { if (fh 0) { binCount 1; for (NodeK,V e f;; binCount) { K ek; // 若节点已经存在修改该节点的值 if (e.hash hash ((ek e.key) key || (ek ! null key.equals(ek)))) { oldVal e.val; if (!onlyIfAbsent) e.val value; break; } NodeK,V pred e; // 节点不存在添加到链表末尾 if ((e e.next) null) { pred.next new NodeK,V(hash, key, value, null); break; } } } // 如果该节点是 红黑树节点 else if (f instanceof TreeBin) { NodeK,V p; binCount 2; if ((p ((TreeBinK,V)f).putTreeVal(hash, key, value)) ! null) { oldVal p.val; if (!onlyIfAbsent) p.val value; } } } } // 链表节点超过了8链表转为红黑树 if (binCount ! 0) { if (binCount TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal ! null) return oldVal; break; } } } // 统计节点个数检查是否需要resize addCount(1L, binCount); return null; }3.1 与 HashMap 的入参差异拒绝 nullif (key null || value null) throw new NullPointerException();HashMap 允许一个 null key 和多个 null value而 ConcurrentHashMap从源头拒绝了 null。原因在于并发环境下 null 具有二义性get返回 null 既可能表示key 不存在也可能表示key 存在但 value 为 null。在无锁并发场景下这种二义性会导致无法区分不存在与还没写入两种状态因此直接禁止 null 更安全。3.2 自旋 三重分支putVal外层是for (;;)无条件的自旋循环每次循环根据当前桶位置的状态走不同分支直到成功写入并跳出分支一桶为空 → CAS 直接写入无锁else if ((f tabAt(tab, i (n - 1) hash)) null) { if (casTabAt(tab, i, null, new NodeK,V(hash, key, value, null))) break; // no lock when adding to empty bin }当指定数组位置无元素时使用 CAS 操作将 Node 键值对放入对应的数组下标。casTabAt是一个CASCompare And Swap比较并交换方法只有当前位置仍为 null 时才把新节点放进去否则自旋重试。整个过程不加锁、不阻塞线程也能保证并发安全——这正是 JDK 1.8 高吞吐的关键多线程并发插入不同空桶时彼此完全无竞争。分支二桶的 hash 为 MOVED → 协助扩容else if ((fh f.hash) MOVED) tab helpTransfer(tab, f);如果读到节点的 hash 值为MOVED-1说明当前 Map 正在扩容且这个桶已经被迁移。此时当前线程不会傻等而是调用helpTransfer协助扩容帮忙搬运其他桶的元素扩容完成后再回来更新值。这种多线程协作迁移设计避免了单线程扩容期间其他线程全部阻塞。分支三hash 冲突 → synchronized 锁住桶头节点synchronized (f) { // f 是 链表头节点/红黑树根节点出现 hash 冲突时使用synchronized 局部锁锁住当前桶的头节点f即链表头节点或红黑树根节点。注意锁的粒度是单个桶而不是整个 Map。不同桶之间互不干扰多个线程即使同时写不同桶也完全并行锁竞争被降到最低。3.3 桶内操作链表与红黑树双路径在synchronized (f)块内部先通过if (tabAt(tab, i) f)做二次确认防止加锁前该桶已被其他线程迁移/替换然后根据节点类型分两条路径处理链表路径fh 0以f为起点遍历链表binCount从 1 开始累计节点数。若找到 hash 与 key 都相等的节点则修改该节点的val除非onlyIfAbsent为 true若遍历到链表末尾仍没找到则在链表末尾追加新节点。红黑树路径f instanceof TreeBin说明该桶已完成树化调用TreeBin.putTreeVal在红黑树上遍历更新已存在节点或插入新节点binCount直接记为 2树化后的桶不再参与链表转树判断。3.4 树化判断与计数// 链表节点超过了8链表转为红黑树 if (binCount ! 0) { if (binCount TREEIFY_THRESHOLD) treeifyBin(tab, i); ... } ... // 统计节点个数检查是否需要resize addCount(1L, binCount);写入成功后若链表节点数超过 8TREEIFY_THRESHOLD调用treeifyBin尝试将链表转为红黑树注意treeifyBin内部还会判断桶数组长度是否 64若数组过短则优先扩容而非树化这是 JDK 1.8 HashMap 与 ConcurrentHashMap 共有的策略细节可参考 HashMap 源码解析最后调用addCount(1L, binCount)统计节点个数并检查是否需要扩容。addCount内部使用了CounterCell数组 baseCount的分散计数结构竞争不激烈时直接 CAS 更新baseCount竞争激烈时为各线程分配独立的CounterCell分散累加最后求和。这与 Sentinel 底层 LongAdder 的计数实现 中剖析的思路同源——把单点计数的竞争压力打散到多个 cell 上从而支撑高并发场景下size()的高效统计。四、JDK 1.7 与 JDK 1.8同步机制对比JDK 1.7 与 JDK 1.8 在同步机制上的区别总结如下对比维度JDK 1.7 ConcurrentHashMapJDK 1.8 ConcurrentHashMap同步策略分段锁SegmentCAS 乐观锁 synchronized 局部锁锁的数据结构内部类Segment继承ReentrantLock直接对桶头节点synchronized (f)锁粒度每段Segment一把锁一段内所有桶共用每个桶一把锁粒度更细并发能力最大并发度 分段数默认 16理论上并发度接近桶数随扩容不断提升空桶写入仍需要获得段锁直接 CAS完全无锁扩容分段内扩容相对简单多线程协助迁移helpTransfer锁竞争成本数据量大时竞争依然激烈只能靠增加锁维持性能即使数据量很大也能保证良好的并发性JDK 1.7 的分段锁机制内部类Segment继承了ReentrantLock将容器内的数组划分成多段区域每个区域对应一把锁相比于 HashTable 确实提升了不少并发能力——HashTable 是全表一把锁所有读写串行分段锁把竞争范围缩小到段内。但在数据量庞大的情况下性能依然不容乐观只能通过不断地增加锁来维持并发性能而增加段数又会带来内存开销与段内空转。JDK 1.8 的 CAS synchronized 方案使用CAS 乐观锁 synchronized 局部锁处理并发问题。空桶用 CAS 无锁写入冲突桶用 synchronized 锁住单个桶头节点锁粒度更细即使数据量很大也能保证良好的并发性。此外 1.8 还引入了红黑树来缓解极端 hash 冲突下链表过长的问题并将扩容改造成多线程协作模式。一句话概括演进脉络HashTable全表锁→ JDK 1.7 分段锁段级锁→ JDK 1.8 CAS 桶级锁锁粒度逐步细化并发能力逐步提升。五、实战ConcurrentHashMap 在主流框架中的典型用法ConcurrentHashMap 并非只是面试题它在主流框架源码中广泛用于并发环境下需要读多写少、且要求吞吐量的缓存与注册表场景。当前仓库的源码笔记中就有直接证据5.1 Spring Cloud Gateway 中的路由缓存在 spring-cloud-gateway 源码笔记 中CachingRouteLocator是CompositeRouteLocator的代理其核心职责就是使用 ConcurrentHashMap 缓存路由Route结果public class CachingRouteLocator implements Ordered, RouteLocator, ApplicationListenerRefreshRoutesEvent, ApplicationEventPublisherAware { private final RouteLocator delegate; private final MapString, List cache new ConcurrentHashMap(); private ApplicationEventPublisher applicationEventPublisher; public CachingRouteLocator(RouteLocator delegate) { // 是这个 CompositeRouteLocator this.delegate delegate; // 使用 ConcurrentHashMap 缓存 Route缓存中没有就执行 fetch 方法得到 routes CacheFlux.lookup(cache, CACHE_KEY, Route.class).onCacheMissResume(this::fetch); } ... }缓存读写并发安全网关的请求路由查找是高并发热点ConcurrentHashMap保证多个请求线程并发读写路由缓存时不会出现脏数据同时读写吞吐远高于 HashTable。缓存与事件联动它实现ApplicationListenerRefreshRoutesEvent接口收到路由刷新事件后更新缓存结果并发布RefreshRoutesResultEvent事件——即缓存失效 → 重新 fetch → 发布刷新结果的完整闭环而这一过程中对缓存的并发访问始终由 ConcurrentHashMap 兜底。类似的用 ConcurrentHashMap 做并发安全缓存/注册表的用法在 Spring 源码中也非常常见。正如仓库中 初级开发者应该从 spring 源码中学什么 提到的读 Spring 源码时经常能看到一些与公司框架有异曲同工之妙的编码技巧及实现比如异常的批量抛出、ConcurrentHashMap 初始化其容量、ThreadLocal 的使用等等。给 ConcurrentHashMap 预设置合理的初始容量以减少扩容正是其中最具实操价值的一条。5.2 业务侧的使用建议源自源码设计结合源码中的实现细节给出几条可直接落地的使用建议预估容量减少扩容能预估元素数量时优先使用new ConcurrentHashMap(expectedSize)并参考tableSizeFor的取整逻辑预留余量避免扩容时多线程协助搬迁带来的额外开销。键值均不可为 null业务代码若依赖 null 作为不存在的语义需要自行调整或用包装对象 哨兵值替代。读多写少的缓存场景是它的主场get无锁、桶内写锁互不干扰的特性使它在高并发读 低频写的路由缓存、配置缓存、注册中心本地缓存等场景中表现优异。六、小结从源码层面看JDK 1.8 的ConcurrentHashMap对并发安全的回答可以浓缩为四个字分层降争。数据结构同源于 HashMap动态数组 链表/红黑树常量与成员变量的设计几乎与 HashMap 相差无几重点差异是volatile修饰的table、nextTable与兼任容量暂存/扩容标识的sizeCtl。空桶 → CAS 无锁写入casTabAt一次比较交换搞定不加锁不阻塞。冲突桶 → synchronized 锁桶头锁粒度细化到单个桶链尾追加 / 更新节点 / 红黑树插入分路径处理binCount 8触发树化。扩容 → 多线程协助helpTransfer让写入线程顺路帮忙搬桶addCount用分散计数支撑高频size()统计。与 JDK 1.7 的对比从Segment ReentrantLock 分段锁到CAS 桶级 synchronized 局部锁锁粒度更细、空桶无锁、并发能力更强。如果想进一步对比它在无锁并发数据结构、原子计数方面的姊妹实现可继续阅读仓库中 详解 AbstractQueuedSynchronizer锁与同步组件的骨架与 Semaphore 源码解析自旋 CAS 的典型运用以及 Sentinel 底层 LongAdder 的计数实现分散计数思路的同源实现从而拼出 J.U.C 并发体系的完整图景。【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考