Java集合-06-ConcurrentHashMap源码(上):JDK8结构、CAS与synchronized定位

发布时间:2026/10/24 7:27:35
Java集合-06-ConcurrentHashMap源码(上):JDK8结构、CAS与synchronized定位 结论先行JDK8 为什么放弃分段锁JDK8 的 ConcurrentHashMap以下简称 CHM放弃了 JDK7 的 Segment 分段锁改用CAS synchronized 红黑树的组合方案。核心变化是锁粒度从「段」细化到「桶」即每个哈希桶bin的头节点作为锁对象配合 CAS 无锁操作在保证线程安全的同时大幅提升并发度。为什么放弃分段锁主要有三个原因锁粒度更细JDK7 中一个 Segment 锁保护多个桶并发写同一 Segment 内的不同桶仍会互斥JDK8 锁粒度到单个桶不同桶的写操作可以完全并行。查询效率更高JDK7 的 get 需要两次哈希定位先定位 Segment再定位桶JDK8 只需一次哈希定位且读操作完全无锁。内存占用更小JDK7 的 Segment 数组本身占用额外内存JDK8 直接使用 Node 数组结构更紧凑。一句话总结JDK8 用「CAS 负责无竞争场景synchronized 负责有竞争场景」把锁的粒度压到最小同时利用红黑树把链表过长时的查询复杂度从 O(n) 降到 O(log n)。核心结构Node、ForwardingNode、TreeBin、TreeNodeJDK8 CHM 的核心数据结构包括以下几个关键类Node哈希桶中链表的基本节点包含 key、value、hash 和 next 指针。value 和 next 都用 volatile 修饰保证可见性。ForwardingNode扩容时用于标记已迁移完成的桶。它的 hash 值为 -1MOVED当线程访问到该节点时会协助完成扩容。TreeBin红黑树的容器节点hash 值为 -2TREEBIN。它持有红黑树的根节点并负责读写锁的协调。TreeNode红黑树节点继承自 Node增加了 parent、left、right、red 等字段。此外还有两个核心字段sizeCtl控制表初始化和扩容的控制字段在不同状态下含义不同详见下文。tableNode 数组即哈希桶数组用 volatile 修饰。JDK7 vs JDK8从 Segment 分段锁到桶级锁JDK7 的 CHM 采用 Segment 分段锁设计内部维护一个 Segment 数组每个 Segment 继承自 ReentrantLock是一个独立的哈希表。写入时先定位 Segment再对 Segment 加锁然后定位桶进行插入。并发度受限于 Segment 数量默认 16不同 Segment 之间可以并行但同一 Segment 内的写操作互斥。JDK8 的 CHM 彻底重构不再有 Segment直接使用 Node 数组 桶级锁。写入时先通过 CAS 尝试无锁插入CAS 失败桶非空才使用 synchronized 锁住头节点。读操作完全无锁利用 volatile 保证可见性。对比表格如下维度JDK7JDK8锁粒度Segment段桶bin锁实现ReentrantLockCAS synchronized读操作无锁但需两次哈希无锁一次哈希查询复杂度链表 O(n)链表 O(n) / 红黑树 O(log n)内存占用Segment 数组额外开销更紧凑JDK8 CHM 结构图JDK8 CHM 的整体结构可以用下图表示flowchart TD CHM[ConcurrentHashMap] -- table[Node 数组 table] table -- b0[桶0: Node 链表] table -- b1[桶1: 空] table -- b2[桶2: TreeBin 红黑树] table -- b3[桶3: ForwardingNode 扩容中] b0 -- n1[Node] n1 -- n2[Node] b2 -- rb[TreeNode 红黑树] CHM -- sizeCtl[sizeCtl 控制字段] CHM -- baseCount[baseCount 计数]初始化 initTableCAS 修改 sizeCtlCHM 采用懒加载策略构造时并不初始化 table 数组而是在第一次 put 时通过 initTable() 完成初始化。核心代码如下private final NodeK,V[] initTable() { NodeK,V[] tab; int sc; while ((tab table) null || tab.length 0) { if ((sc sizeCtl) 0) Thread.yield(); // 其他线程正在初始化让出 CPU else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // CAS 将 sizeCtl 从正数改为 -1表示当前线程独占初始化 try { if ((tab table) null || tab.length 0) { int n (sc 0) ? sc : DEFAULT_CAPACITY; SuppressWarnings(unchecked) NodeK,V[] nt (NodeK,V[])new Node?,?[n]; table tab nt; sc n - (n 2); // 扩容阈值 0.75 * n } } finally { sizeCtl sc; } break; } } return tab; }关键点多个线程同时进入 initTable() 时只有一个线程能通过 CAS 把 sizeCtl 从正数改为 -1获得初始化权。其他线程看到 sizeCtl 0直接 Thread.yield() 让出 CPU避免自旋空转。初始化完成后sizeCtl 被设置为扩容阈值0.75 倍容量。懒加载与 get无锁读为什么可以不锁get 操作完全无锁核心代码如下public V get(Object key) { NodeK,V[] tab; NodeK,V e, p; int n, eh; K ek; int h spread(key.hashCode()); 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; } else if (eh 0) // 遇到 ForwardingNode 或 TreeBin 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; }为什么 get 可以不锁核心原因有三个volatile 保证可见性table 数组引用、Node 的 val 和 next 都用 volatile 修饰读线程能立即看到其他线程的写入结果。tabAt 使用 Unsafe.getObjectVolatile读取桶头节点时使用 volatile 读保证读到的是最新值。链表节点一旦插入就不会修改 next 指向put 操作只会新增节点不会修改已有节点的 next 引用除了树化时整体替换因此读线程遍历链表时不会读到不一致的中间状态。需要注意的是get 读到的是「某一时刻的近似值」并不保证强一致。如果读线程遍历链表的同时写线程正在树化该桶读线程可能读到旧链表但不会读到损坏数据。put 流程从 CAS 到 synchronizedputVal() 是 CHM 写入的核心方法完整流程如下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; K fk; V fv; if (tab null || (n tab.length) 0) tab initTable(); // 1. 表为空先初始化 else if ((f tabAt(tab, i (n - 1) hash)) null) { // 2. 桶为空CAS 直接放入新节点 if (casTabAt(tab, i, null, new NodeK,V(hash, key, value))) break; } else if ((fh f.hash) MOVED) tab helpTransfer(tab, f); // 3. 桶正在扩容协助迁移 else { V oldVal null; synchronized (f) { // 4. 锁住头节点 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); 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; } } else if (f instanceof ReservationNode) throw new IllegalStateException(Recursive update); } } if (binCount ! 0) { // 5. 链表长度达到阈值树化 if (binCount TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal ! null) return oldVal; break; } } } addCount(1L, binCount); // 6. 更新元素计数 return null; }put 流程可以归纳为四步表为空调用 initTable() 初始化。桶为空通过 CAS 直接放入新节点无锁完成。hash MOVED桶正在扩容调用 helpTransfer() 协助迁移。桶非空synchronized 锁住头节点在链表或红黑树中插入。put 流程图flowchart TD A[putVal 开始] -- B{table 为空?} B -- 是 -- C[initTable 初始化] C -- D{桶为空?} B -- 否 -- D D -- 是 -- E[CAS 放入新节点] E -- F[addCount 更新计数] D -- 否 -- G{hash MOVED?} G -- 是 -- H[helpTransfer 协助扩容] H -- F G -- 否 -- I[synchronized 锁头节点] I -- J{链表 or 红黑树?} J -- 链表 -- K[遍历链表插入] J -- 红黑树 -- L[putTreeVal 插入] K -- M{binCount 8?} L -- F M -- 是 -- N[treeifyBin 树化] N -- F M -- 否 -- F树化binCount TREEIFY_THRESHOLD当链表长度达到阈值默认 8时CHM 会调用 treeifyBin() 将链表转换为红黑树。但树化还有一个前置条件table 长度必须大于等于 MIN_TREEIFY_CAPACITY默认 64。如果 table 长度不足 64会优先触发扩容而不是树化。为什么是 8这是基于泊松分布的统计结果在负载因子 0.75 的前提下链表长度达到 8 的概率极低约千万分之一此时说明哈希分布严重不均用红黑树替代链表可以保证查询效率。树化过程将链表节点逐个转换为 TreeNode。通过 TreeBin 容器持有红黑树根节点。桶头节点替换为 TreeBin 对象hash 值为 -2。addCount 与 sizeCtl 含义put 完成后调用 addCount() 更新元素计数。CHM 的计数采用baseCount CounterCell 数组的方式无竞争时直接 CAS 更新 baseCount竞争激烈时分散到 CounterCell 数组中减少 CAS 冲突。sizeCtl 是 CHM 最核心的控制字段不同取值含义如下sizeCtl 值含义0默认值表示尚未初始化-1正在初始化某个线程独占负数-(1n)正在扩容n 表示参与扩容的线程数正数下次扩容的阈值容量 × 0.75sizeCtl 状态图flowchart LR S0[0: 未初始化] --|CAS 置为 -1| S1[-1: 初始化中] S1 --|初始化完成| S2[正数: 扩容阈值] S2 --|触发扩容, 置为 -(1n)| S3[负数: 扩容中] S3 --|扩容完成| S2面试题与总结以下是本章的核心面试题建议重点掌握1. JDK8 CHM 怎么保证线程安全JDK8 CHM 通过三种机制协同保证线程安全CAS用于无竞争场景如桶为空时的节点插入、sizeCtl 的修改、计数的更新。synchronized用于有竞争场景锁住桶头节点保护链表或红黑树的插入操作。volatile保证

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询